From 34eb6d54263cd0ae426f021f98092e6daacbcd70 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 29 Dec 2015 17:36:32 -0500 Subject: [PATCH] Add support for @ExceptionHandler methods --- .../reactive/ReactorServerHttpResponse.java | 9 +- .../reactive/RxNettyServerHttpRequest.java | 1 - .../reactive/RxNettyServerHttpResponse.java | 11 +-- .../reactive/ServletServerHttpResponse.java | 8 +- .../reactive/UndertowServerHttpResponse.java | 16 ++-- .../server/reactive/WriteWithOperator.java | 38 ++++++-- .../web/reactive/DispatcherHandler.java | 20 ++++- .../web/reactive/HandlerResult.java | 56 +++++++++++- .../method/InvocableHandlerMethod.java | 23 ++--- .../RequestMappingHandlerAdapter.java | 88 ++++++++++++++++--- .../AbstractHttpHandlerIntegrationTests.java | 1 - .../reactive/WriteWithOperatorTests.java | 2 +- .../RequestMappingIntegrationTests.java | 43 +++++++++ 13 files changed, 265 insertions(+), 51 deletions(-) diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 17ab111345c..3233d38daae 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -25,7 +25,6 @@ import reactor.io.net.http.model.Status; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; /** @@ -67,9 +66,11 @@ public class ReactorServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher setBody(Publisher contentPublisher) { - applyHeaders(); - return this.channel.writeWith(Publishers.map(contentPublisher, Buffer::new)); + public Publisher setBody(Publisher publisher) { + return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { + applyHeaders(); + return this.channel.writeWith(Publishers.map(writePublisher, Buffer::new)); + })); } private void applyHeaders() { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java index 1fa794ca377..cf075d9f3fc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpRequest.java @@ -28,7 +28,6 @@ import rx.Observable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; /** diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java index 73ac4864cee..78f71415892 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/RxNettyServerHttpResponse.java @@ -28,7 +28,6 @@ import rx.Observable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.Assert; /** @@ -72,10 +71,12 @@ public class RxNettyServerHttpResponse implements ServerHttpResponse { @Override public Publisher setBody(Publisher publisher) { - applyHeaders(); - Observable observable = RxJava1Converter.from(publisher).map( - content -> new Buffer(content).asBytes()); - return RxJava1Converter.from(this.response.writeBytes(observable)); + return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { + applyHeaders(); + Observable observable = RxJava1Converter.from(writePublisher) + .map(buffer -> new Buffer(buffer).asBytes()); + return RxJava1Converter.from(this.response.writeBytes(observable)); + })); } private void applyHeaders() { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index b992cc64631..984f9dd9cad 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -83,9 +83,11 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } @Override - public Publisher setBody(final Publisher contentPublisher) { - applyHeaders(); - return (s -> contentPublisher.subscribe(subscriber)); + public Publisher setBody(final Publisher publisher) { + return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { + applyHeaders(); + return (s -> writePublisher.subscribe(subscriber)); + })); } private void applyHeaders() { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java index d96b85bdf32..01f1d9ee534 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java @@ -38,6 +38,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import org.xnio.ChannelListener; import org.xnio.channels.StreamSinkChannel; +import reactor.Publishers; import reactor.core.subscriber.BaseSubscriber; import static org.xnio.ChannelListeners.closingChannelExceptionHandler; @@ -74,13 +75,6 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { this.exchange.setStatusCode(status.value()); } - - @Override - public Publisher setBody(Publisher bodyPublisher) { - applyHeaders(); - return (subscriber -> bodyPublisher.subscribe(bodySubscriber)); - } - @Override public HttpHeaders getHeaders() { return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); @@ -112,6 +106,14 @@ public class UndertowServerHttpResponse implements ServerHttpResponse { } } + @Override + public Publisher setBody(Publisher publisher) { + return Publishers.lift(publisher, new WriteWithOperator<>(writePublisher -> { + applyHeaders(); + return (subscriber -> writePublisher.subscribe(bodySubscriber)); + })); + } + private class ResponseBodySubscriber extends BaseSubscriber implements ChannelListener { diff --git a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java index 5aa3371b70f..2f23abf73c6 100644 --- a/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java +++ b/spring-web-reactive/src/main/java/org/springframework/http/server/reactive/WriteWithOperator.java @@ -97,7 +97,7 @@ public class WriteWithOperator implements Function, else if (this.beforeFirstEmission) { this.item = item; this.beforeFirstEmission = false; - writeFunction.apply(this).subscribe(downstream()); + writeFunction.apply(this).subscribe(new DownstreamBridge(downstream())); } else { subscription.cancel(); @@ -139,7 +139,7 @@ public class WriteWithOperator implements Function, else if (this.beforeFirstEmission) { this.completed = true; this.beforeFirstEmission = false; - writeFunction.apply(this).subscribe(downstream()); + writeFunction.apply(this).subscribe(new DownstreamBridge(downstream())); } else { this.completed = true; @@ -148,10 +148,10 @@ public class WriteWithOperator implements Function, } @Override - public void subscribe(Subscriber subscriber) { + public void subscribe(Subscriber writeSubscriber) { synchronized (this) { Assert.isNull(this.writeSubscriber, "Only one writeSubscriber supported."); - this.writeSubscriber = subscriber; + this.writeSubscriber = writeSubscriber; if (this.error != null || this.completed) { this.writeSubscriber.onSubscribe(NO_OP_SUBSCRIPTION); @@ -184,7 +184,7 @@ public class WriteWithOperator implements Function, @Override protected void doRequest(long n) { - if (this.readyToWrite) { + if (readyToWrite) { super.doRequest(n); return; } @@ -204,6 +204,34 @@ public class WriteWithOperator implements Function, } } + private class DownstreamBridge implements Subscriber { + + private final Subscriber downstream; + + public DownstreamBridge(Subscriber downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Void aVoid) { + } + + @Override + public void onError(Throwable ex) { + this.downstream.onError(ex); + } + + @Override + public void onComplete() { + this.downstream.onComplete(); + } + } + private final static Subscription NO_OP_SUBSCRIPTION = new Subscription() { @Override 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 2b654e1564e..ee3bfe70e3e 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 @@ -129,8 +129,24 @@ public class DispatcherHandler implements HttpHandler, ApplicationContextAware { }); Publisher completionPublisher = Publishers.concatMap(resultPublisher, result -> { - HandlerResultHandler handler = getResultHandler(result); - return handler.handleResult(request, response, result); + Publisher publisher; + if (result.hasError()) { + publisher = Publishers.error(result.getError()); + } + else { + HandlerResultHandler handler = getResultHandler(result); + publisher = handler.handleResult(request, response, result); + } + if (result.hasExceptionMapper()) { + return Publishers.onErrorResumeNext(publisher, ex -> { + return Publishers.concatMap(result.getExceptionMapper().apply(ex), + errorResult -> { + HandlerResultHandler handler = getResultHandler(errorResult); + return handler.handleResult(request, response, errorResult); + }); + }); + } + return publisher; }); return mapError(completionPublisher, this.errorMapper); diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 0dacf869e73..4a6bae92abe 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -16,11 +16,17 @@ package org.springframework.web.reactive; +import java.util.function.Function; +import java.util.logging.Handler; + +import org.reactivestreams.Publisher; +import reactor.Publishers; + import org.springframework.core.ResolvableType; import org.springframework.util.Assert; /** - * Represent the result of the invocation of an handler. + * Represent the result of the invocation of a handler. * * @author Rossen Stoyanchev */ @@ -32,6 +38,10 @@ public class HandlerResult { private final ResolvableType resultType; + private final Throwable error; + + private Function> exceptionMapper; + public HandlerResult(Object handler, Object result, ResolvableType resultType) { Assert.notNull(handler, "'handler' is required"); @@ -39,6 +49,16 @@ public class HandlerResult { this.handler = handler; this.result = result; this.resultType = resultType; + this.error = null; + } + + public HandlerResult(Object handler, Throwable error) { + Assert.notNull(handler, "'handler' is required"); + Assert.notNull(error, "'error' is required"); + this.handler = handler; + this.result = null; + this.resultType = null; + this.error = error; } @@ -54,4 +74,38 @@ public class HandlerResult { return this.resultType; } + public Throwable getError() { + return this.error; + } + + /** + * Whether handler invocation produced a result or failed with an error. + *

If {@code true} the {@link #getError()} returns the error while + * {@link #getResult()} and {@link #getResultType()} return {@code null} + * and vice versa. + * @return whether this instance contains a result or an error. + */ + public boolean hasError() { + return (this.error != null); + } + + /** + * Configure a function for selecting an alternate {@code HandlerResult} in + * case of an {@link #hasError() error result} or in case of an async result + * that results in an error. + * @param function the exception resolving function + */ + public HandlerResult setExceptionMapper(Function> function) { + this.exceptionMapper = function; + return this; + } + + public Function> getExceptionMapper() { + return this.exceptionMapper; + } + + public boolean hasExceptionMapper() { + return (this.exceptionMapper != null); + } + } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java index 5b89c254e43..5cebab6252f 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/InvocableHandlerMethod.java @@ -62,6 +62,10 @@ public class InvocableHandlerMethod extends HandlerMethod { super(handlerMethod); } + public InvocableHandlerMethod(Object bean, Method method) { + super(bean, method); + } + public void setHandlerMethodArgumentResolvers(List resolvers) { this.resolvers.clear(); @@ -75,9 +79,10 @@ public class InvocableHandlerMethod extends HandlerMethod { /** - * - * @param request - * @param providedArgs + * Invoke the method and return a Publisher for the return value. + * @param request the current request + * @param providedArgs optional list of argument values to check by type + * (via {@code instanceof}) for resolving method arguments. * @return Publisher that produces a single HandlerResult or an error signal; * never throws an exception. */ @@ -98,11 +103,8 @@ public class InvocableHandlerMethod extends HandlerMethod { return Publishers.concatMap(argsPublisher, args -> { try { Object value = doInvoke(args); - - HandlerMethod handlerMethod = InvocableHandlerMethod.this; - ResolvableType type = ResolvableType.forMethodParameter(handlerMethod.getReturnType()); - HandlerResult handlerResult = new HandlerResult(handlerMethod, value, type); - + ResolvableType type = ResolvableType.forMethodParameter(getReturnType()); + HandlerResult handlerResult = new HandlerResult(this, value, type); return Publishers.just(handlerResult); } catch (InvocationTargetException ex) { @@ -187,9 +189,8 @@ public class InvocableHandlerMethod extends HandlerMethod { } private static Publisher mapError(Publisher source, Function function) { - return Publishers.lift(source, null, (throwable, subscriber) -> { - subscriber.onError(function.apply(throwable)); - }, null); + return Publishers.lift(source, null, + (throwable, subscriber) -> subscriber.onError(function.apply(throwable)), null); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java index d997913e7de..de6106177dd 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/method/annotation/RequestMappingHandlerAdapter.java @@ -16,30 +16,35 @@ package org.springframework.web.reactive.method.annotation; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.Publishers; import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.ResolvableType; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.JacksonJsonDecoder; import org.springframework.core.codec.support.JsonObjectDecoder; import org.springframework.core.codec.support.StringDecoder; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.ObjectUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.method.HandlerMethodArgumentResolver; import org.springframework.web.reactive.method.InvocableHandlerMethod; -import org.springframework.web.method.HandlerMethod; /** @@ -47,16 +52,29 @@ import org.springframework.web.method.HandlerMethod; */ public class RequestMappingHandlerAdapter implements HandlerAdapter, InitializingBean { + private static Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class); + + private final List argumentResolvers = new ArrayList<>(); private ConversionService conversionService = new DefaultConversionService(); + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = + new ConcurrentHashMap, ExceptionHandlerMethodResolver>(64); + + /** + * Configure the complete list of supported argument types thus overriding + * the resolvers that would otherwise be configured by default. + */ public void setArgumentResolvers(List resolvers) { this.argumentResolvers.clear(); this.argumentResolvers.addAll(resolvers); } + /** + * Return the configured argument resolvers. + */ public List getArgumentResolvers() { return this.argumentResolvers; } @@ -91,9 +109,59 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin public Publisher handle(ServerHttpRequest request, ServerHttpResponse response, Object handler) { - InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod((HandlerMethod) handler); - handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); - return handlerMethod.invokeForRequest(request); + HandlerMethod handlerMethod = (HandlerMethod) handler; + + InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); + invocable.setHandlerMethodArgumentResolvers(this.argumentResolvers); + + Publisher publisher = invocable.invokeForRequest(request); + + publisher = Publishers.onErrorResumeNext(publisher, ex -> { + return Publishers.just(new HandlerResult(handler, ex)); + }); + + publisher = Publishers.map(publisher, + result -> result.setExceptionMapper( + ex -> mapException((Exception) ex, handlerMethod, request, response))); + + return publisher; + } + + private Publisher mapException(Throwable ex, HandlerMethod handlerMethod, + ServerHttpRequest request, ServerHttpResponse response) { + + if (ex instanceof Exception) { + InvocableHandlerMethod invocable = findExceptionHandler(handlerMethod, (Exception) ex); + if (invocable != null) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Invoking @ExceptionHandler method: " + invocable); + } + invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers()); + return invocable.invokeForRequest(request, response, ex); + } + catch (Exception invocationEx) { + if (logger.isErrorEnabled()) { + logger.error("Failed to invoke @ExceptionHandler method: " + invocable, invocationEx); + } + } + } + } + return Publishers.error(ex); + } + + protected InvocableHandlerMethod findExceptionHandler(HandlerMethod handlerMethod, Exception exception) { + if (handlerMethod == null) { + return null; + } + Class handlerType = handlerMethod.getBeanType(); + ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType); + if (resolver == null) { + resolver = new ExceptionHandlerMethodResolver(handlerType); + this.exceptionHandlerCache.put(handlerType, resolver); + } + Method method = resolver.resolveMethod(exception); + return (method != null ? new InvocableHandlerMethod(handlerMethod.getBean(), method) : null); } } \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java index fb11e7b1519..83781e29f9b 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/AbstractHttpHandlerIntegrationTests.java @@ -21,7 +21,6 @@ import org.junit.Before; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.boot.HttpServer; import org.springframework.http.server.reactive.boot.JettyHttpServer; import org.springframework.http.server.reactive.boot.ReactorHttpServer; diff --git a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java index ac847a7f99c..2ad1ddd3d47 100644 --- a/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/http/server/reactive/WriteWithOperatorTests.java @@ -31,7 +31,7 @@ import reactor.Publishers; import reactor.core.publisher.PublisherFactory; import reactor.core.subscriber.SubscriberBarrier; import reactor.rx.Streams; -import reactor.rx.action.Signal; +import reactor.rx.stream.Signal; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java index 15d6d8febaf..50ac1bd8736 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/method/annotation/RequestMappingIntegrationTests.java @@ -55,6 +55,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -122,6 +123,30 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati assertEquals("Hello!", response.getBody()); } + @Test + public void handleWithThrownException() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/thrown-exception"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, String.class); + + assertEquals("Recovered from error: Boo", response.getBody()); + } + + @Test + public void handleWithErrorSignal() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + + URI url = new URI("http://localhost:" + port + "/error-signal"); + RequestEntity request = RequestEntity.get(url).build(); + ResponseEntity response = restTemplate.exchange(request, String.class); + + assertEquals("Recovered from error: Boo", response.getBody()); + } + @Test public void serializeAsPojo() throws Exception { serializeAsPojo("http://localhost:" + port + "/person"); @@ -478,6 +503,24 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati return personStream.toList().doOnNext(persons::addAll).flatMap(document -> Observable.empty()); } + @RequestMapping("/thrown-exception") + @ResponseBody + public Publisher handleAndThrowException() { + throw new IllegalStateException("Boo"); + } + + @RequestMapping("/error-signal") + @ResponseBody + public Publisher handleWithError() { + return Publishers.error(new IllegalStateException("Boo")); + } + + @ExceptionHandler + @ResponseBody + public Publisher handleException(IllegalStateException ex) { + return Streams.just("Recovered from error: " + ex.getMessage()); + } + //TODO add mixed and T request mappings tests }