diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java index 444f14a7c9..1e68b4890d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandler.java @@ -19,11 +19,14 @@ package org.springframework.web.reactive; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; import reactor.Publishers; +import reactor.fn.BiConsumer; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; @@ -63,12 +66,30 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { private List resultHandlers; + private Function errorMapper = new DispatcherHandlerExceptionMapper(); + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { initStrategies(applicationContext); } + /** + * Configure a function to map error signals from the {@code DispatcherHandler}. + *

By default this is set to {@link DispatcherHandlerExceptionMapper}. + * @param errorMapper the function + */ + public void setErrorMapper(Function errorMapper) { + this.errorMapper = errorMapper; + } + + /** + * Return the configured function for mapping exceptions. + */ + public Function getErrorMapper() { + return this.errorMapper; + } + protected void initStrategies(ApplicationContext context) { Map mappingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( @@ -107,10 +128,12 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { return handlerAdapter.handle(request, response, handler); }); - return Publishers.concatMap(resultPublisher, result -> { + Publisher completionPublisher = Publishers.concatMap(resultPublisher, result -> { HandlerResultHandler handler = getResultHandler(result); return handler.handleResult(request, response, result); }); + + return mapError(completionPublisher, this.errorMapper); } protected HandlerAdapter getHandlerAdapter(Object handler) { @@ -139,6 +162,15 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { }); } + private static Publisher mapError(Publisher source, Function function) { + return Publishers.lift(source, null, new BiConsumer>() { + @Override + public void accept(Throwable throwable, Subscriber subscriber) { + subscriber.onError(function.apply(throwable)); + } + }, null); + } + private static class NotFoundHandlerMapping implements HandlerMapping { @Override diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java new file mode 100644 index 0000000000..9884aa122d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapper.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2015 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; + +import java.util.function.Function; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.http.HttpStatus; +import org.springframework.web.ResponseStatusException; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * Map "standard" framework exceptions and + * {@link ResponseStatus @ResponseStatus}-annotated exceptions to a + * {@link ResponseStatusException}. + * + * @author Rossen Stoyanchev + */ +public class DispatcherHandlerExceptionMapper implements Function { + + + @Override + public Throwable apply(Throwable ex) { + if (ex instanceof HandlerNotFoundException) { + ex = new ResponseStatusException(HttpStatus.NOT_FOUND, ex); + } + else if (ex instanceof HttpMediaTypeNotAcceptableException) { + ex = new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE, ex); + } + else { + ResponseStatus status = findStatus(ex); + if (status != null) { + ex = new ResponseStatusException(status.code(), ex); + } + } + return ex; + } + + private ResponseStatus findStatus(Throwable ex) { + Class type = ex.getClass(); + ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(type, ResponseStatus.class); + if (status != null) { + return status; + } + else if (ex.getCause() != null) { + return findStatus(ex.getCause()); + } + return null; + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index 0027fd31d6..00f378c0f6 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -37,6 +37,7 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.web.ResponseStatusException; import org.springframework.http.server.reactive.ErrorHandlingHttpHandler; import org.springframework.http.server.reactive.FilterChainHttpHandler; import org.springframework.http.server.reactive.HttpExceptionHandler; @@ -58,6 +59,7 @@ import org.springframework.web.reactive.method.annotation.ResponseBodyResultHand import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; @@ -101,7 +103,9 @@ public class DispatcherHandlerErrorTests { Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); Throwable ex = awaitErrorSignal(publisher); - assertEquals(HandlerNotFoundException.class, ex.getClass()); + assertEquals(ResponseStatusException.class, ex.getClass()); + assertNotNull(ex.getCause()); + assertEquals(HandlerNotFoundException.class, ex.getCause().getClass()); } @Test @@ -155,7 +159,9 @@ public class DispatcherHandlerErrorTests { Publisher publisher = this.dispatcherHandler.handle(this.request, this.response); Throwable ex = awaitErrorSignal(publisher); - assertEquals(HttpMediaTypeNotAcceptableException.class, ex.getClass()); + assertEquals(ResponseStatusException.class, ex.getClass()); + assertNotNull(ex.getCause()); + assertEquals(HttpMediaTypeNotAcceptableException.class, ex.getCause().getClass()); } @Test diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java new file mode 100644 index 0000000000..afa5744009 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/DispatcherHandlerExceptionMapperTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2015 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; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.ResponseStatusException; +import org.springframework.web.bind.annotation.ResponseStatus; + +import static org.junit.Assert.assertEquals; + +/** + * @author Rossen Stoyanchev + */ +public class DispatcherHandlerExceptionMapperTests { + + private DispatcherHandlerExceptionMapper mapper; + + + @Before + public void setUp() throws Exception { + this.mapper = new DispatcherHandlerExceptionMapper(); + } + + + @Test + public void handlerNotFound() throws Exception { + Throwable ex = new HandlerNotFoundException(HttpMethod.GET, "/path", new HttpHeaders()); + ex = this.mapper.apply(ex); + + assertEquals(ResponseStatusException.class, ex.getClass()); + assertEquals(HttpStatus.NOT_FOUND, ((ResponseStatusException) ex).getHttpStatus()); + } + + + @Test + public void httpMediaTypeNotAcceptable() throws Exception { + Throwable ex = new HttpMediaTypeNotAcceptableException(Collections.emptyList()); + ex = this.mapper.apply(ex); + + assertEquals(ResponseStatusException.class, ex.getClass()); + assertEquals(HttpStatus.NOT_ACCEPTABLE, ((ResponseStatusException) ex).getHttpStatus()); + } + + @Test + public void responseStatusAnnotation() throws Exception { + Throwable ex = new ResponseStatusAnnotatedException(); + ex = this.mapper.apply(ex); + + assertEquals(ResponseStatusException.class, ex.getClass()); + assertEquals(HttpStatus.BAD_REQUEST, ((ResponseStatusException) ex).getHttpStatus()); + } + + @Test + public void responseStatusAnnotationOnRootCause() throws Exception { + Throwable ex = new Exception(new ResponseStatusAnnotatedException()); + ex = this.mapper.apply(ex); + + assertEquals(ResponseStatusException.class, ex.getClass()); + assertEquals(HttpStatus.BAD_REQUEST, ((ResponseStatusException) ex).getHttpStatus()); + } + + + @ResponseStatus(code = HttpStatus.BAD_REQUEST) + private static class ResponseStatusAnnotatedException extends Exception { + } + +}