parent
448aac813a
commit
4ba3d0736f
4 changed files with 300 additions and 38 deletions
@ -0,0 +1,73 @@ |
|||||||
|
/* |
||||||
|
* 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.http.server.reactive; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import org.reactivestreams.Publisher; |
||||||
|
import reactor.Publishers; |
||||||
|
import reactor.core.publisher.convert.RxJava1Converter; |
||||||
|
import rx.Observable; |
||||||
|
|
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link HttpHandler} that delegates to a target {@link HttpHandler} and handles |
||||||
|
* any errors from it by invoking one or more {@link HttpExceptionHandler}s |
||||||
|
* sequentially until one of them completes successfully. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class ErrorHandlingHttpHandler extends HttpHandlerDecorator { |
||||||
|
|
||||||
|
private final List<HttpExceptionHandler> exceptionHandlers; |
||||||
|
|
||||||
|
|
||||||
|
public ErrorHandlingHttpHandler(HttpHandler targetHandler, HttpExceptionHandler... exceptionHandlers) { |
||||||
|
super(targetHandler); |
||||||
|
Assert.notEmpty(exceptionHandlers, "At least one exception handler is required"); |
||||||
|
this.exceptionHandlers = Arrays.asList(exceptionHandlers); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public Publisher<Void> handle(ServerHttpRequest request, ServerHttpResponse response) { |
||||||
|
Publisher<Void> publisher; |
||||||
|
try { |
||||||
|
publisher = getDelegate().handle(request, response); |
||||||
|
} |
||||||
|
catch (Throwable ex) { |
||||||
|
publisher = Publishers.error(ex); |
||||||
|
} |
||||||
|
for (HttpExceptionHandler handler : this.exceptionHandlers) { |
||||||
|
publisher = applyExceptionHandler(publisher, handler, request, response); |
||||||
|
} |
||||||
|
return publisher; |
||||||
|
} |
||||||
|
|
||||||
|
private static Publisher<Void> applyExceptionHandler(Publisher<Void> publisher, |
||||||
|
HttpExceptionHandler handler, ServerHttpRequest request, ServerHttpResponse response) { |
||||||
|
|
||||||
|
// see https://github.com/reactor/reactor/issues/580
|
||||||
|
|
||||||
|
Observable<Void> observable = RxJava1Converter.from(publisher).onErrorResumeNext(ex -> { |
||||||
|
return RxJava1Converter.from(handler.handle(request, response, ex)); |
||||||
|
}); |
||||||
|
return RxJava1Converter.from(observable); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
/* |
||||||
|
* 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.http.server.reactive; |
||||||
|
|
||||||
|
import org.reactivestreams.Publisher; |
||||||
|
|
||||||
|
/** |
||||||
|
* A contract for resolving exceptions from HTTP request handling. |
||||||
|
* |
||||||
|
* <p>{@link ErrorHandlingHttpHandler} provides a way of applying a list |
||||||
|
* {@link HttpExceptionHandler}s to a target {@link HttpHandler}. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @see ErrorHandlingHttpHandler |
||||||
|
*/ |
||||||
|
public interface HttpExceptionHandler { |
||||||
|
|
||||||
|
/** |
||||||
|
* Handle the given exception and return a completion Publisher to indicate |
||||||
|
* when error handling is complete, or send an error signal if the exception |
||||||
|
* was not handled. |
||||||
|
* |
||||||
|
* @param request the current request |
||||||
|
* @param response the current response |
||||||
|
* @param ex the exception to handle |
||||||
|
* @return Publisher to indicate when exception handling is complete. |
||||||
|
*/ |
||||||
|
Publisher<Void> handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,168 @@ |
|||||||
|
/* |
||||||
|
* 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.http.server.reactive; |
||||||
|
|
||||||
|
|
||||||
|
import java.net.URI; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
|
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Test; |
||||||
|
import org.reactivestreams.Publisher; |
||||||
|
import reactor.Publishers; |
||||||
|
import reactor.rx.Streams; |
||||||
|
import reactor.rx.action.Signal; |
||||||
|
|
||||||
|
import org.springframework.http.HttpMethod; |
||||||
|
import org.springframework.http.HttpStatus; |
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals; |
||||||
|
import static org.junit.Assert.assertNull; |
||||||
|
import static org.junit.Assert.assertTrue; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
@SuppressWarnings("ThrowableResultOfMethodCallIgnored") |
||||||
|
public class ErrorHandlingHttpHandlerTests { |
||||||
|
|
||||||
|
private MockServerHttpRequest request; |
||||||
|
|
||||||
|
private MockServerHttpResponse response; |
||||||
|
|
||||||
|
|
||||||
|
@Before |
||||||
|
public void setUp() throws Exception { |
||||||
|
this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("http://localhost:8080")); |
||||||
|
this.response = new MockServerHttpResponse(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Test |
||||||
|
public void handleErrorSignal() throws Exception { |
||||||
|
HttpExceptionHandler exceptionHandler = new UnresolvedExceptionHandler(); |
||||||
|
HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); |
||||||
|
HttpHandler handler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); |
||||||
|
|
||||||
|
Publisher<Void> publisher = handler.handle(this.request, this.response); |
||||||
|
Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); |
||||||
|
|
||||||
|
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void handleErrorSignalWithMultipleHttpErrorHandlers() throws Exception { |
||||||
|
HttpExceptionHandler[] exceptionHandlers = new HttpExceptionHandler[] { |
||||||
|
new UnresolvedExceptionHandler(), |
||||||
|
new UnresolvedExceptionHandler(), |
||||||
|
new HttpStatusExceptionHandler(HttpStatus.INTERNAL_SERVER_ERROR), |
||||||
|
new UnresolvedExceptionHandler() |
||||||
|
}; |
||||||
|
HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); |
||||||
|
HttpHandler httpHandler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandlers); |
||||||
|
|
||||||
|
Publisher<Void> publisher = httpHandler.handle(this.request, this.response); |
||||||
|
Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); |
||||||
|
|
||||||
|
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void unresolvedException() throws Exception { |
||||||
|
HttpExceptionHandler exceptionHandler = new UnresolvedExceptionHandler(); |
||||||
|
HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo")); |
||||||
|
HttpHandler httpHandler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); |
||||||
|
|
||||||
|
Publisher<Void> publisher = httpHandler.handle(this.request, this.response); |
||||||
|
Throwable ex = awaitErrorSignal(publisher); |
||||||
|
|
||||||
|
assertEquals("boo", ex.getMessage()); |
||||||
|
assertNull(this.response.getStatus()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void thrownExceptionBecomesErrorSignal() throws Exception { |
||||||
|
HttpExceptionHandler exceptionHandler = new HttpStatusExceptionHandler(HttpStatus.INTERNAL_SERVER_ERROR); |
||||||
|
HttpHandler targetHandler = new TestHttpHandler(new IllegalStateException("boo"), true); |
||||||
|
HttpHandler handler = new ErrorHandlingHttpHandler(targetHandler, exceptionHandler); |
||||||
|
|
||||||
|
Publisher<Void> publisher = handler.handle(this.request, this.response); |
||||||
|
Streams.wrap(publisher).toList().await(5, TimeUnit.SECONDS); |
||||||
|
|
||||||
|
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, this.response.getStatus()); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private Throwable awaitErrorSignal(Publisher<?> publisher) throws Exception { |
||||||
|
Signal<?> signal = Streams.wrap(publisher).materialize().toList().await(5, TimeUnit.SECONDS).get(0); |
||||||
|
assertEquals("Unexpected signal: " + signal, Signal.Type.ERROR, signal.getType()); |
||||||
|
return signal.getThrowable(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static class TestHttpHandler implements HttpHandler { |
||||||
|
|
||||||
|
private final Throwable exception; |
||||||
|
|
||||||
|
private final boolean raise; |
||||||
|
|
||||||
|
|
||||||
|
public TestHttpHandler(Throwable exception) { |
||||||
|
this(exception, false); |
||||||
|
} |
||||||
|
|
||||||
|
public TestHttpHandler(Throwable exception, boolean raise) { |
||||||
|
this.exception = exception; |
||||||
|
this.raise = raise; |
||||||
|
assertTrue(exception instanceof RuntimeException || !this.raise); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Publisher<Void> handle(ServerHttpRequest request, ServerHttpResponse response) { |
||||||
|
if (this.raise) { |
||||||
|
throw (RuntimeException) exception; |
||||||
|
} |
||||||
|
return Publishers.error(this.exception); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** Leave the exception unresolved. */ |
||||||
|
private static class UnresolvedExceptionHandler implements HttpExceptionHandler { |
||||||
|
|
||||||
|
@Override |
||||||
|
public Publisher<Void> handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { |
||||||
|
return Publishers.error(ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** Set the response status to the given HttpStatus. */ |
||||||
|
private static class HttpStatusExceptionHandler implements HttpExceptionHandler { |
||||||
|
|
||||||
|
private final HttpStatus status; |
||||||
|
|
||||||
|
public HttpStatusExceptionHandler(HttpStatus status) { |
||||||
|
this.status = status; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Publisher<Void> handle(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) { |
||||||
|
response.setStatusCode(this.status); |
||||||
|
return Publishers.empty(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue