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:19:58 +02:00
committed by Brian Clozel
parent b4d9b514f5
commit d6f5356db1
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();