From ba5ef6456faa1ce6f39c8f5474ebf56cb5d07052 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 21 Mar 2018 16:12:32 +0100 Subject: [PATCH] WebFluxResponseStatusExceptionHandler for @ResponseStatus introspection The web.server package is quite low-level and should not depend on web.bind in order to avoid a dependency cycle. Extracting the introspection of the ResponseStatus annotation into a WebFlux-level subclass resolves the cycle. Issue: SPR-16567 --- .../ResponseStatusExceptionHandler.java | 49 +++++++------- .../ResponseStatusExceptionHandlerTests.java | 42 +++++------- .../config/WebFluxConfigurationSupport.java | 4 +- .../DefaultHandlerStrategiesBuilder.java | 6 +- ...WebFluxResponseStatusExceptionHandler.java | 54 +++++++++++++++ ...uxResponseStatusExceptionHandlerTests.java | 65 +++++++++++++++++++ 6 files changed, 167 insertions(+), 53 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/handler/WebFluxResponseStatusExceptionHandler.java create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/handler/WebFluxResponseStatusExceptionHandlerTests.java diff --git a/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java b/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java index d760c8676ca..5093a28be8c 100644 --- a/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java +++ b/spring-web/src/main/java/org/springframework/web/server/handler/ResponseStatusExceptionHandler.java @@ -20,22 +20,15 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; -import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.lang.Nullable; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; /** - * Handle instances of {@link ResponseStatusException}, or of exceptions annotated - * with {@link ResponseStatus @ResponseStatus}, by extracting the - * {@code HttpStatus} from them and updating the status of the response. - * - *

If the response is already committed, the error remains unresolved and is - * propagated. + * Handle {@link ResponseStatusException} by setting the response status. * * @author Rossen Stoyanchev * @author Sebastien Deleuze @@ -48,7 +41,7 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler { @Override public Mono handle(ServerWebExchange exchange, Throwable ex) { - HttpStatus status = resolveHttpStatus(ex); + HttpStatus status = resolveStatus(ex); if (status != null && exchange.getResponse().setStatusCode(status)) { if (status.is5xxServerError()) { logger.error(buildMessage(exchange.getRequest(), ex)); @@ -64,24 +57,34 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler { return Mono.error(ex); } + private String buildMessage(ServerHttpRequest request, Throwable ex) { + return "Failed to handle request [" + request.getMethod() + " " + request.getURI() + "]: " + ex.getMessage(); + } + @Nullable - private HttpStatus resolveHttpStatus(Throwable ex) { + private HttpStatus resolveStatus(Throwable ex) { + HttpStatus status = determineStatus(ex); + if (status == null) { + Throwable cause = ex.getCause(); + if (cause != null) { + status = resolveStatus(cause); + } + } + return status; + } + + /** + * Determine the HTTP status implied by the given exception. + * @param ex the exception to introspect + * @return the associated HTTP status, if any + * @since 5.0.5 + */ + @Nullable + protected HttpStatus determineStatus(Throwable ex) { if (ex instanceof ResponseStatusException) { return ((ResponseStatusException) ex).getStatus(); } - ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class); - if (status != null) { - return status.code(); - } - if (ex.getCause() != null) { - return resolveHttpStatus(ex.getCause()); - } return null; } - private String buildMessage(ServerHttpRequest request, Throwable ex) { - return "Failed to handle request [" + request.getMethod() + " " - + request.getURI() + "]: " + ex.getMessage(); - } - -} \ No newline at end of file +} diff --git a/spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java b/spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java index d81f1cb138e..3224cefc434 100644 --- a/spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/handler/ResponseStatusExceptionHandlerTests.java @@ -18,6 +18,7 @@ package org.springframework.web.server.handler; import java.time.Duration; +import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -25,21 +26,31 @@ import reactor.test.StepVerifier; import org.springframework.http.HttpStatus; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.web.test.server.MockServerWebExchange; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.server.ResponseStatusException; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; +import static org.junit.Assert.*; /** * Unit tests for {@link ResponseStatusExceptionHandler}. + * * @author Rossen Stoyanchev + * @author Juergen Hoeller */ public class ResponseStatusExceptionHandlerTests { - private final ResponseStatusExceptionHandler handler = new ResponseStatusExceptionHandler(); + protected final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); - private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + protected ResponseStatusExceptionHandler handler; + + + @Before + public void setup() { + this.handler = createResponseStatusExceptionHandler(); + } + + protected ResponseStatusExceptionHandler createResponseStatusExceptionHandler() { + return new ResponseStatusExceptionHandler(); + } @Test @@ -49,13 +60,6 @@ public class ResponseStatusExceptionHandlerTests { assertEquals(HttpStatus.BAD_REQUEST, this.exchange.getResponse().getStatusCode()); } - @Test - public void handleAnnotatedException() { - Throwable ex = new CustomException(); - this.handler.handle(this.exchange, ex).block(Duration.ofSeconds(5)); - assertEquals(HttpStatus.I_AM_A_TEAPOT, this.exchange.getResponse().getStatusCode()); - } - @Test public void handleNestedResponseStatusException() { Throwable ex = new Exception(new ResponseStatusException(HttpStatus.BAD_REQUEST, "")); @@ -63,13 +67,6 @@ public class ResponseStatusExceptionHandlerTests { assertEquals(HttpStatus.BAD_REQUEST, this.exchange.getResponse().getStatusCode()); } - @Test - public void handleNestedAnnotatedException() { - Throwable ex = new Exception(new CustomException()); - this.handler.handle(this.exchange, ex).block(Duration.ofSeconds(5)); - assertEquals(HttpStatus.I_AM_A_TEAPOT, this.exchange.getResponse().getStatusCode()); - } - @Test public void unresolvedException() { Throwable expected = new IllegalStateException(); @@ -77,7 +74,7 @@ public class ResponseStatusExceptionHandlerTests { StepVerifier.create(mono).consumeErrorWith(actual -> assertSame(expected, actual)).verify(); } - @Test // SPR-16231 + @Test // SPR-16231 public void responseCommitted() { Throwable ex = new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Oops"); this.exchange.getResponse().setStatusCode(HttpStatus.CREATED); @@ -86,9 +83,4 @@ public class ResponseStatusExceptionHandlerTests { StepVerifier.create(mono).consumeErrorWith(actual -> assertSame(ex, actual)).verify(); } - - @ResponseStatus(HttpStatus.I_AM_A_TEAPOT) - private static class CustomException extends Exception { - } - } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java index 5715552b376..c636d929a28 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java @@ -52,6 +52,7 @@ import org.springframework.web.reactive.function.server.support.HandlerFunctionA import org.springframework.web.reactive.function.server.support.RouterFunctionMapping; import org.springframework.web.reactive.function.server.support.ServerResponseResultHandler; import org.springframework.web.reactive.handler.AbstractHandlerMapping; +import org.springframework.web.reactive.handler.WebFluxResponseStatusExceptionHandler; import org.springframework.web.reactive.result.SimpleHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; @@ -62,7 +63,6 @@ import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; -import org.springframework.web.server.handler.ResponseStatusExceptionHandler; import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; import org.springframework.web.server.i18n.LocaleContextResolver; @@ -108,7 +108,7 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { @Bean @Order(0) public WebExceptionHandler responseStatusExceptionHandler() { - return new ResponseStatusExceptionHandler(); + return new WebFluxResponseStatusExceptionHandler(); } @Bean diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java index 697425cb4c6..824ad3855ab 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,10 +25,10 @@ import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.Assert; +import org.springframework.web.reactive.handler.WebFluxResponseStatusExceptionHandler; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.WebExceptionHandler; import org.springframework.web.server.WebFilter; -import org.springframework.web.server.handler.ResponseStatusExceptionHandler; import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; import org.springframework.web.server.i18n.LocaleContextResolver; @@ -58,7 +58,7 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { public void defaultConfiguration() { this.codecConfigurer.registerDefaults(true); - this.exceptionHandlers.add(new ResponseStatusExceptionHandler()); + this.exceptionHandlers.add(new WebFluxResponseStatusExceptionHandler()); this.localeContextResolver = new AcceptHeaderLocaleContextResolver(); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/WebFluxResponseStatusExceptionHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/WebFluxResponseStatusExceptionHandler.java new file mode 100644 index 00000000000..2bc38d9fd89 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/WebFluxResponseStatusExceptionHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.handler; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.server.handler.ResponseStatusExceptionHandler; + +/** + * Common WebFlux exception handler that detects instances of + * {@link org.springframework.web.server.ResponseStatusException} + * (inherited from the base class) as well as exceptions annotated with + * {@link ResponseStatus @ResponseStatus} by determining the HTTP status + * for them and updating the status of the response accordingly. + * + *

If the response is already committed, the error remains unresolved + * and is propagated. + * + * @author Juergen Hoeller + * @author Rossen Stoyanchev + * @since 5.0.5 + */ +public class WebFluxResponseStatusExceptionHandler extends ResponseStatusExceptionHandler { + + @Override + @Nullable + protected HttpStatus determineStatus(Throwable ex) { + HttpStatus status = super.determineStatus(ex); + if (status == null) { + ResponseStatus ann = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class); + if (ann != null) { + status = ann.code(); + } + } + return status; + } + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/handler/WebFluxResponseStatusExceptionHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/handler/WebFluxResponseStatusExceptionHandlerTests.java new file mode 100644 index 00000000000..025b7715d12 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/handler/WebFluxResponseStatusExceptionHandlerTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.handler; + +import java.time.Duration; + +import org.junit.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.reactive.handler.WebFluxResponseStatusExceptionHandler; +import org.springframework.web.server.handler.ResponseStatusExceptionHandler; +import org.springframework.web.server.handler.ResponseStatusExceptionHandlerTests; + +import static org.junit.Assert.*; + +/** + * Unit tests for {@link WebFluxResponseStatusExceptionHandler}. + * + * @author Juergen Hoeller + * @author Rossen Stoyanchev + */ +public class WebFluxResponseStatusExceptionHandlerTests extends ResponseStatusExceptionHandlerTests { + + @Override + protected ResponseStatusExceptionHandler createResponseStatusExceptionHandler() { + return new WebFluxResponseStatusExceptionHandler(); + } + + + @Test + public void handleAnnotatedException() { + Throwable ex = new CustomException(); + this.handler.handle(this.exchange, ex).block(Duration.ofSeconds(5)); + assertEquals(HttpStatus.I_AM_A_TEAPOT, this.exchange.getResponse().getStatusCode()); + } + + @Test + public void handleNestedAnnotatedException() { + Throwable ex = new Exception(new CustomException()); + this.handler.handle(this.exchange, ex).block(Duration.ofSeconds(5)); + assertEquals(HttpStatus.I_AM_A_TEAPOT, this.exchange.getResponse().getStatusCode()); + } + + + @SuppressWarnings("serial") + @ResponseStatus(HttpStatus.I_AM_A_TEAPOT) + private static class CustomException extends Exception { + } + +}