Skip to content

Commit 2878ade

Browse files
committed
WebFlux support for handling of early exceptions
This change enables a WebFlux HandlerAdapter to handle not only the success scenario when a handler is selected, but also any potential error signal that may occur instead. This makes it possible to extend ControllerAdvice support to exceptions from handler mapping such as a 404, 406, 415, and/or even earlier exceptions from the WebFilter chain. Closes gh-22991
1 parent 9d73f81 commit 2878ade

File tree

6 files changed

+180
-52
lines changed

6 files changed

+180
-52
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.reactive;
18+
19+
import reactor.core.publisher.Mono;
20+
21+
import org.springframework.web.server.ServerWebExchange;
22+
23+
/**
24+
* Contract to map a {@link Throwable} to a {@link HandlerResult}.
25+
*
26+
* @author Rossen Stoyanchev
27+
* @since 6.0
28+
*/
29+
public interface DispatchExceptionHandler {
30+
31+
/**
32+
* Handler the given exception and resolve it to {@link HandlerResult} that
33+
* can be used for rendering an HTTP response.
34+
* @param exchange the current exchange
35+
* @param ex the exception to handle
36+
* @return a {@code Mono} that emits a {@code HandlerResult} or the original exception
37+
*/
38+
Mono<HandlerResult> handleError(ServerWebExchange exchange, Throwable ex);
39+
40+
}

spring-webflux/src/main/java/org/springframework/web/reactive/DispatcherHandler.java

+23-11
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ public Mono<Void> handle(ServerWebExchange exchange) {
150150
.concatMap(mapping -> mapping.getHandler(exchange))
151151
.next()
152152
.switchIfEmpty(createNotFoundError())
153-
.flatMap(handler -> invokeHandler(exchange, handler))
154-
.flatMap(result -> handleResult(exchange, result));
153+
.onErrorResume(ex -> handleDispatchError(exchange, ex))
154+
.flatMap(handler -> handleRequestWith(exchange, handler));
155155
}
156156

157157
private <R> Mono<R> createNotFoundError() {
@@ -161,14 +161,27 @@ private <R> Mono<R> createNotFoundError() {
161161
});
162162
}
163163

164-
private Mono<HandlerResult> invokeHandler(ServerWebExchange exchange, Object handler) {
164+
private Mono<Void> handleDispatchError(ServerWebExchange exchange, Throwable ex) {
165+
Mono<HandlerResult> resultMono = Mono.error(ex);
166+
if (this.handlerAdapters != null) {
167+
for (HandlerAdapter adapter : this.handlerAdapters) {
168+
if (adapter instanceof DispatchExceptionHandler exceptionHandler) {
169+
resultMono = resultMono.onErrorResume(ex2 -> exceptionHandler.handleError(exchange, ex2));
170+
}
171+
}
172+
}
173+
return resultMono.flatMap(result -> handleResult(exchange, result));
174+
}
175+
176+
private Mono<Void> handleRequestWith(ServerWebExchange exchange, Object handler) {
165177
if (ObjectUtils.nullSafeEquals(exchange.getResponse().getStatusCode(), HttpStatus.FORBIDDEN)) {
166178
return Mono.empty(); // CORS rejection
167179
}
168180
if (this.handlerAdapters != null) {
169-
for (HandlerAdapter handlerAdapter : this.handlerAdapters) {
170-
if (handlerAdapter.supports(handler)) {
171-
return handlerAdapter.handle(exchange, handler);
181+
for (HandlerAdapter adapter : this.handlerAdapters) {
182+
if (adapter.supports(handler)) {
183+
return adapter.handle(exchange, handler)
184+
.flatMap(result -> handleResult(exchange, result));
172185
}
173186
}
174187
}
@@ -179,11 +192,10 @@ private Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result
179192
return getResultHandler(result).handleResult(exchange, result)
180193
.checkpoint("Handler " + result.getHandler() + " [DispatcherHandler]")
181194
.onErrorResume(ex ->
182-
result.applyExceptionHandler(ex).flatMap(exResult -> {
183-
String text = "Exception handler " + exResult.getHandler() +
184-
", error=\"" + ex.getMessage() + "\" [DispatcherHandler]";
185-
return getResultHandler(exResult).handleResult(exchange, exResult).checkpoint(text);
186-
}));
195+
result.applyExceptionHandler(ex).flatMap(exResult ->
196+
getResultHandler(exResult).handleResult(exchange, exResult)
197+
.checkpoint("Exception handler " + exResult.getHandler() + ", " +
198+
"error=\"" + ex.getMessage() + "\" [DispatcherHandler]")));
187199
}
188200

189201
private HandlerResultHandler getResultHandler(HandlerResult handlerResult) {

spring-webflux/src/main/java/org/springframework/web/reactive/HandlerAdapter.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,14 @@
2626
* Contract that decouples the {@link DispatcherHandler} from the details of
2727
* invoking a handler and makes it possible to support any handler type.
2828
*
29+
* <p>A {@code HandlerAdapter} can implement {@link DispatchExceptionHandler}
30+
* if it wants to handle an exception that occured before the request is mapped
31+
* to a handler. This allows the {@code HandlerAdapter} to expose a consistent
32+
* exception handling mechanism for any request handling error.
33+
* In Reactive Streams terms, {@link #handle} processes the onNext, while
34+
* {@link DispatchExceptionHandler#handleError} processes the onError signal
35+
* from the upstream.
36+
*
2937
* @author Rossen Stoyanchev
3038
* @author Sebastien Deleuze
3139
* @since 5.0

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java

+26-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -330,38 +330,47 @@ private InvocableHandlerMethod createAttributeMethod(Object bean, Method method)
330330
}
331331

332332
/**
333-
* Find an {@code @ExceptionHandler} method in {@code @ControllerAdvice}
334-
* components or in the controller of the given {@code @RequestMapping} method.
333+
* Look for an {@code @ExceptionHandler} method within the class of the given
334+
* controller method, and also within {@code @ControllerAdvice} classes that
335+
* are applicable to the class of the given controller method.
336+
* @param ex the exception to find a handler for
337+
* @param handlerMethod the controller method that raised the exception, or
338+
* if {@code null}, check only {@code @ControllerAdvice} classes.
335339
*/
336340
@Nullable
337-
public InvocableHandlerMethod getExceptionHandlerMethod(Throwable ex, HandlerMethod handlerMethod) {
338-
Class<?> handlerType = handlerMethod.getBeanType();
339-
340-
// Controller-local first...
341-
Object targetBean = handlerMethod.getBean();
342-
Method targetMethod = this.exceptionHandlerCache
343-
.computeIfAbsent(handlerType, ExceptionHandlerMethodResolver::new)
344-
.resolveMethodByThrowable(ex);
341+
public InvocableHandlerMethod getExceptionHandlerMethod(Throwable ex, @Nullable HandlerMethod handlerMethod) {
342+
343+
Class<?> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null);
344+
Object exceptionHandlerObject = null;
345+
Method exceptionHandlerMethod = null;
346+
347+
if (handlerType != null) {
348+
// Controller-local first...
349+
exceptionHandlerObject = handlerMethod.getBean();
350+
exceptionHandlerMethod = this.exceptionHandlerCache
351+
.computeIfAbsent(handlerType, ExceptionHandlerMethodResolver::new)
352+
.resolveMethodByThrowable(ex);
353+
}
345354

346-
if (targetMethod == null) {
355+
if (exceptionHandlerMethod == null) {
347356
// Global exception handlers...
348357
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
349358
ControllerAdviceBean advice = entry.getKey();
350359
if (advice.isApplicableToBeanType(handlerType)) {
351-
targetBean = advice.resolveBean();
352-
targetMethod = entry.getValue().resolveMethodByThrowable(ex);
353-
if (targetMethod != null) {
360+
exceptionHandlerMethod = entry.getValue().resolveMethodByThrowable(ex);
361+
if (exceptionHandlerMethod != null) {
362+
exceptionHandlerObject = advice.resolveBean();
354363
break;
355364
}
356365
}
357366
}
358367
}
359368

360-
if (targetMethod == null) {
369+
if (exceptionHandlerObject == null || exceptionHandlerMethod == null) {
361370
return null;
362371
}
363372

364-
InvocableHandlerMethod invocable = new InvocableHandlerMethod(targetBean, targetMethod);
373+
InvocableHandlerMethod invocable = new InvocableHandlerMethod(exceptionHandlerObject, exceptionHandlerMethod);
365374
invocable.setArgumentResolvers(this.exceptionHandlerResolvers);
366375
return invocable;
367376
}

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java

+21-6
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.web.bind.support.WebBindingInitializer;
3939
import org.springframework.web.method.HandlerMethod;
4040
import org.springframework.web.reactive.BindingContext;
41+
import org.springframework.web.reactive.DispatchExceptionHandler;
4142
import org.springframework.web.reactive.HandlerAdapter;
4243
import org.springframework.web.reactive.HandlerMapping;
4344
import org.springframework.web.reactive.HandlerResult;
@@ -52,7 +53,8 @@
5253
* @author Rossen Stoyanchev
5354
* @since 5.0
5455
*/
55-
public class RequestMappingHandlerAdapter implements HandlerAdapter, ApplicationContextAware, InitializingBean {
56+
public class RequestMappingHandlerAdapter
57+
implements HandlerAdapter, DispatchExceptionHandler, ApplicationContextAware, InitializingBean {
5658

5759
private static final Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class);
5860

@@ -193,7 +195,7 @@ public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
193195
InvocableHandlerMethod invocableMethod = this.methodResolver.getRequestMappingMethod(handlerMethod);
194196

195197
Function<Throwable, Mono<HandlerResult>> exceptionHandler =
196-
ex -> handleException(ex, handlerMethod, bindingContext, exchange);
198+
ex -> handleException(exchange, ex, handlerMethod, bindingContext);
197199

198200
return this.modelInitializer
199201
.initModel(handlerMethod, bindingContext, exchange)
@@ -203,23 +205,31 @@ public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
203205
.onErrorResume(exceptionHandler);
204206
}
205207

206-
private Mono<HandlerResult> handleException(Throwable exception, HandlerMethod handlerMethod,
207-
BindingContext bindingContext, ServerWebExchange exchange) {
208+
private Mono<HandlerResult> handleException(
209+
ServerWebExchange exchange, Throwable exception,
210+
@Nullable HandlerMethod handlerMethod, @Nullable BindingContext bindingContext) {
208211

209212
Assert.state(this.methodResolver != null, "Not initialized");
210213

211214
// Success and error responses may use different content types
212215
exchange.getAttributes().remove(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
213216
exchange.getResponse().getHeaders().clearContentHeaders();
214217

215-
InvocableHandlerMethod invocable = this.methodResolver.getExceptionHandlerMethod(exception, handlerMethod);
218+
InvocableHandlerMethod invocable =
219+
this.methodResolver.getExceptionHandlerMethod(exception, handlerMethod);
220+
216221
if (invocable != null) {
217222
ArrayList<Throwable> exceptions = new ArrayList<>();
218223
try {
219224
if (logger.isDebugEnabled()) {
220225
logger.debug(exchange.getLogPrefix() + "Using @ExceptionHandler " + invocable);
221226
}
222-
bindingContext.getModel().asMap().clear();
227+
if (bindingContext != null) {
228+
bindingContext.getModel().asMap().clear();
229+
}
230+
else {
231+
bindingContext = new BindingContext();
232+
}
223233

224234
// Expose causes as provided arguments as well
225235
Throwable exToExpose = exception;
@@ -245,4 +255,9 @@ private Mono<HandlerResult> handleException(Throwable exception, HandlerMethod h
245255
return Mono.error(exception);
246256
}
247257

258+
@Override
259+
public Mono<HandlerResult> handleError(ServerWebExchange exchange, Throwable ex) {
260+
return handleException(exchange, ex, null, null);
261+
}
262+
248263
}

0 commit comments

Comments
 (0)