From 07cbd482a0006d42eef61cede2b8a125c2b2a748 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 26 Jun 2026 17:19:55 +0100 Subject: [PATCH] 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 --- .../function/server/RouterFunctions.java | 18 +++++- .../function/server/RouterFunctionsTests.java | 55 +++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java index e1d469a6419..d6faabedce3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java @@ -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 Mono handlePreFlightRequest() { + return (Mono) ServerResponse.status(HttpStatus.FORBIDDEN).build(); + } } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionsTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionsTests.java index af891328cf3..5d08a460089 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionsTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionsTests.java @@ -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 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 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();