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();