Add preflight handling in RouterFunctionWebHandler

Request predicates support preflight request matching based on the "would be"
request (e.g. target HTTP method) so the actual handler is not meant to be
invoked. That's the case only with a DispatcherHandler setup.

Closes gh-37024
This commit is contained in:
rstoyanchev
2026-08-14 09:11:50 +02:00
committed by Brian Clozel
parent dadd474d21
commit 07cbd482a0
2 changed files with 70 additions and 3 deletions
@@ -38,6 +38,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.Assert;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
@@ -1475,9 +1476,14 @@ public abstract class RouterFunctions {
addAttributes(exchange, request);
return this.routerFunction.route(request)
.switchIfEmpty(createNotFoundError())
.flatMap(handlerFunction -> wrapException(() -> handlerFunction.handle(request)))
.flatMap(response -> wrapException(() -> response.writeTo(exchange,
new HandlerStrategiesResponseContext(this.strategies))));
.flatMap(handlerFunction -> wrapException(() -> {
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return handlePreFlightRequest();
}
return handlerFunction.handle(request);
}))
.flatMap(response -> wrapException(() ->
response.writeTo(exchange, new HandlerStrategiesResponseContext(this.strategies))));
});
}
@@ -1498,5 +1504,11 @@ public abstract class RouterFunctions {
return Mono.error(ex);
}
}
@SuppressWarnings("unchecked")
private static <T extends ServerResponse> Mono<T> handlePreFlightRequest() {
return (Mono<T>) ServerResponse.status(HttpStatus.FORBIDDEN).build();
}
}
}
@@ -28,11 +28,14 @@ import reactor.test.StepVerifier;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
@@ -282,6 +285,58 @@ class RouterFunctionsTests {
assertThat(httpResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
void toHttpHandlerPreFlightRequestDefaultHandling() {
RouterFunction<ServerResponse> routerFunction =
RouterFunctions.route(RequestPredicates.all(), request -> ServerResponse.accepted().build());
HttpHandler handler = RouterFunctions.toHttpHandler(routerFunction);
assertThat(handler).isNotNull();
MockServerHttpRequest httpRequest = MockServerHttpRequest.options("https://localhost")
.header("Origin", "https://example.com")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PUT")
.build();
MockServerHttpResponse httpResponse = new MockServerHttpResponse();
handler.handle(httpRequest, httpResponse).block();
assertThat(httpResponse.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
void toHttpHandlerPreFlightRequestHandled() {
CorsWebFilter corsFilter = new CorsWebFilter(exchange -> {
if (exchange.getRequest().getPath().value().equals("/path")) {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.addAllowedOrigin("https://example.com");
corsConfig.addAllowedMethod(HttpMethod.PUT);
return corsConfig;
}
return null;
});
RouterFunction<ServerResponse> routerFunction = RouterFunctions.route()
.PUT("/path", request -> {
throw new IllegalStateException("Not expected");
})
.build();
HttpHandler handler = RouterFunctions.toHttpHandler(
routerFunction, HandlerStrategies.builder().webFilter(corsFilter).build());
MockServerHttpRequest httpRequest = MockServerHttpRequest.options("https://localhost/path")
.header("Origin", "https://example.com")
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PUT")
.build();
MockServerHttpResponse httpResponse = new MockServerHttpResponse();
handler.handle(httpRequest, httpResponse).block();
assertThat(httpResponse.getStatusCode()).isNull();
}
@Test
void toHttpHandlerWebFilter() {
AtomicBoolean filterInvoked = new AtomicBoolean();