Skip to content

Commit e6d206b

Browse files
committed
Extra information in WebFlux stacktraces
Use the checkpoint operator at various places in WebFlux to insert information that Reactor then uses to enrich exceptions, via suppressed exceptions, when error signals flow through the operator. Closes spring-projectsgh-22105
1 parent 495ba2f commit e6d206b

File tree

16 files changed

+144
-45
lines changed

16 files changed

+144
-45
lines changed

spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.ArrayList;
2222
import java.util.Arrays;
2323
import java.util.List;
24+
import java.util.StringJoiner;
2425
import java.util.stream.Collectors;
2526
import java.util.stream.IntStream;
2627

@@ -89,6 +90,8 @@ public class HandlerMethod {
8990
@Nullable
9091
private volatile List<Annotation[][]> interfaceParameterAnnotations;
9192

93+
private final String description;
94+
9295

9396
/**
9497
* Create an instance from a bean instance and a method.
@@ -103,6 +106,7 @@ public HandlerMethod(Object bean, Method method) {
103106
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
104107
this.parameters = initMethodParameters();
105108
evaluateResponseStatus();
109+
this.description = initDescription(this.beanType, this.method);
106110
}
107111

108112
/**
@@ -119,6 +123,7 @@ public HandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
119123
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method);
120124
this.parameters = initMethodParameters();
121125
evaluateResponseStatus();
126+
this.description = initDescription(this.beanType, this.method);
122127
}
123128

124129
/**
@@ -141,6 +146,7 @@ public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
141146
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
142147
this.parameters = initMethodParameters();
143148
evaluateResponseStatus();
149+
this.description = initDescription(this.beanType, this.method);
144150
}
145151

146152
/**
@@ -156,6 +162,7 @@ protected HandlerMethod(HandlerMethod handlerMethod) {
156162
this.parameters = handlerMethod.parameters;
157163
this.responseStatus = handlerMethod.responseStatus;
158164
this.responseStatusReason = handlerMethod.responseStatusReason;
165+
this.description = handlerMethod.description;
159166
this.resolvedFromHandlerMethod = handlerMethod.resolvedFromHandlerMethod;
160167
}
161168

@@ -174,6 +181,7 @@ private HandlerMethod(HandlerMethod handlerMethod, Object handler) {
174181
this.responseStatus = handlerMethod.responseStatus;
175182
this.responseStatusReason = handlerMethod.responseStatusReason;
176183
this.resolvedFromHandlerMethod = handlerMethod;
184+
this.description = handlerMethod.description;
177185
}
178186

179187
private MethodParameter[] initMethodParameters() {
@@ -198,6 +206,14 @@ private void evaluateResponseStatus() {
198206
}
199207
}
200208

209+
private static String initDescription(Class<?> beanType, Method method) {
210+
StringJoiner joiner = new StringJoiner(", ", "(", ")");
211+
for (Class<?> paramType : method.getParameterTypes()) {
212+
joiner.add(paramType.getSimpleName());
213+
}
214+
return beanType.getName() + "#" + method.getName() + joiner.toString();
215+
}
216+
201217

202218
/**
203219
* Return the bean for this handler method.
@@ -389,7 +405,7 @@ public int hashCode() {
389405

390406
@Override
391407
public String toString() {
392-
return this.method.toGenericString();
408+
return this.description;
393409
}
394410

395411

spring-web/src/main/java/org/springframework/web/server/handler/DefaultWebFilterChain.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 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.
@@ -53,7 +53,7 @@ public class DefaultWebFilterChain implements WebFilterChain {
5353
private final WebFilter currentFilter;
5454

5555
@Nullable
56-
private final DefaultWebFilterChain next;
56+
private final DefaultWebFilterChain chain;
5757

5858

5959
/**
@@ -68,7 +68,7 @@ public DefaultWebFilterChain(WebHandler handler, List<WebFilter> filters) {
6868
this.handler = handler;
6969
DefaultWebFilterChain chain = initChain(filters, handler);
7070
this.currentFilter = chain.currentFilter;
71-
this.next = chain.next;
71+
this.chain = chain.chain;
7272
}
7373

7474
private static DefaultWebFilterChain initChain(List<WebFilter> filters, WebHandler handler) {
@@ -84,12 +84,12 @@ private static DefaultWebFilterChain initChain(List<WebFilter> filters, WebHandl
8484
* Private constructor to represent one link in the chain.
8585
*/
8686
private DefaultWebFilterChain(List<WebFilter> allFilters, WebHandler handler,
87-
@Nullable WebFilter currentFilter, @Nullable DefaultWebFilterChain next) {
87+
@Nullable WebFilter currentFilter, @Nullable DefaultWebFilterChain chain) {
8888

8989
this.allFilters = allFilters;
9090
this.currentFilter = currentFilter;
9191
this.handler = handler;
92-
this.next = next;
92+
this.chain = chain;
9393
}
9494

9595
/**
@@ -117,9 +117,14 @@ public WebHandler getHandler() {
117117
@Override
118118
public Mono<Void> filter(ServerWebExchange exchange) {
119119
return Mono.defer(() ->
120-
this.currentFilter != null && this.next != null ?
121-
this.currentFilter.filter(exchange, this.next) :
120+
this.currentFilter != null && this.chain != null ?
121+
invokeFilter(this.currentFilter, this.chain, exchange) :
122122
this.handler.handle(exchange));
123123
}
124124

125+
private Mono<Void> invokeFilter(WebFilter current, DefaultWebFilterChain chain, ServerWebExchange exchange) {
126+
return current.filter(exchange, chain)
127+
.checkpoint(current.getClass().getName() + " [DefaultWebFilterChain]");
128+
}
129+
125130
}

spring-web/src/main/java/org/springframework/web/server/handler/ExceptionHandlingWebHandler.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 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.
@@ -22,6 +22,9 @@
2222

2323
import reactor.core.publisher.Mono;
2424

25+
import org.springframework.http.HttpMethod;
26+
import org.springframework.http.server.reactive.ServerHttpRequest;
27+
import org.springframework.util.StringUtils;
2528
import org.springframework.web.server.ServerWebExchange;
2629
import org.springframework.web.server.WebExceptionHandler;
2730
import org.springframework.web.server.WebHandler;
@@ -41,7 +44,10 @@ public class ExceptionHandlingWebHandler extends WebHandlerDecorator {
4144

4245
public ExceptionHandlingWebHandler(WebHandler delegate, List<WebExceptionHandler> handlers) {
4346
super(delegate);
44-
this.exceptionHandlers = Collections.unmodifiableList(new ArrayList<>(handlers));
47+
List<WebExceptionHandler> handlersToUse = new ArrayList<>();
48+
handlersToUse.add(new CheckpointInsertingHandler());
49+
handlersToUse.addAll(handlers);
50+
this.exceptionHandlers = Collections.unmodifiableList(handlersToUse);
4551
}
4652

4753

@@ -71,4 +77,24 @@ public Mono<Void> handle(ServerWebExchange exchange) {
7177
return completion;
7278
}
7379

80+
81+
/**
82+
* WebExceptionHandler to insert a checkpoint with current URL information.
83+
* Must be the first in order to ensure we catch the error signal before
84+
* the exception is handled and e.g. turned into an error response.
85+
* @since 5.2
86+
*/
87+
private static class CheckpointInsertingHandler implements WebExceptionHandler {
88+
89+
@Override
90+
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
91+
ServerHttpRequest request = exchange.getRequest();
92+
String rawQuery = request.getURI().getRawQuery();
93+
String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : "";
94+
HttpMethod httpMethod = request.getMethod();
95+
String description = "HTTP " + httpMethod + " \"" + request.getPath() + query + "\"";
96+
return Mono.error(ex).checkpoint(description + " [ExceptionHandlingWebHandler]").cast(Void.class);
97+
}
98+
}
99+
74100
}

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 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.
@@ -67,11 +67,6 @@
6767
*/
6868
public class DispatcherHandler implements WebHandler, ApplicationContextAware {
6969

70-
@SuppressWarnings("ThrowableInstanceNeverThrown")
71-
private static final Exception HANDLER_NOT_FOUND_EXCEPTION =
72-
new ResponseStatusException(HttpStatus.NOT_FOUND, "No matching handler");
73-
74-
7570
@Nullable
7671
private List<HandlerMapping> handlerMappings;
7772

@@ -172,8 +167,13 @@ private Mono<HandlerResult> invokeHandler(ServerWebExchange exchange, Object han
172167

173168
private Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
174169
return getResultHandler(result).handleResult(exchange, result)
175-
.onErrorResume(ex -> result.applyExceptionHandler(ex).flatMap(exceptionResult ->
176-
getResultHandler(exceptionResult).handleResult(exchange, exceptionResult)));
170+
.checkpoint("Handler " + result.getHandler() + " [DispatcherHandler]")
171+
.onErrorResume(ex ->
172+
result.applyExceptionHandler(ex).flatMap(exResult -> {
173+
String text = "Exception handler " + exResult.getHandler() +
174+
", error=\"" + ex.getMessage() + "\" [DispatcherHandler]";
175+
return getResultHandler(exResult).handleResult(exchange, exResult).checkpoint(text);
176+
}));
177177
}
178178

179179
private HandlerResultHandler getResultHandler(HandlerResult handlerResult) {

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponse.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 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.
@@ -56,12 +56,17 @@ class DefaultClientResponse implements ClientResponse {
5656

5757
private final String logPrefix;
5858

59+
private final String requestDescription;
60+
61+
62+
public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies,
63+
String logPrefix, String requestDescription) {
5964

60-
public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies, String logPrefix) {
6165
this.response = response;
6266
this.strategies = strategies;
6367
this.headers = new DefaultHeaders();
6468
this.logPrefix = logPrefix;
69+
this.requestDescription = requestDescription;
6570
}
6671

6772

@@ -90,22 +95,35 @@ public MultiValueMap<String, ResponseCookie> cookies() {
9095
return this.response.getCookies();
9196
}
9297

98+
@SuppressWarnings("unchecked")
9399
@Override
94100
public <T> T body(BodyExtractor<T, ? super ClientHttpResponse> extractor) {
95-
return extractor.extract(this.response, new BodyExtractor.Context() {
101+
T result = extractor.extract(this.response, new BodyExtractor.Context() {
96102
@Override
97103
public List<HttpMessageReader<?>> messageReaders() {
98104
return strategies.messageReaders();
99105
}
106+
100107
@Override
101108
public Optional<ServerHttpResponse> serverResponse() {
102109
return Optional.empty();
103110
}
111+
104112
@Override
105113
public Map<String, Object> hints() {
106114
return Hints.from(Hints.LOG_PREFIX_HINT, logPrefix);
107115
}
108116
});
117+
String description = "Body from " + this.requestDescription + " [DefaultClientResponse]";
118+
if (result instanceof Mono) {
119+
return (T) ((Mono<?>) result).checkpoint(description);
120+
}
121+
else if (result instanceof Flux) {
122+
return (T) ((Flux<?>) result).checkpoint(description);
123+
}
124+
else {
125+
return result;
126+
}
109127
}
110128

111129
@Override

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 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.
@@ -136,7 +136,7 @@ public ClientResponse build() {
136136
// When building ClientResponse manually, the ClientRequest.logPrefix() has to be passed,
137137
// e.g. via ClientResponse.Builder, but this (builder) is not used currently.
138138

139-
return new DefaultClientResponse(httpResponse, this.strategies, "");
139+
return new DefaultClientResponse(httpResponse, this.strategies, "", "");
140140
}
141141

142142

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,9 @@ public Mono<ClientResponse> exchange() {
316316
ClientRequest request = (this.inserter != null ?
317317
initRequestBuilder().body(this.inserter).build() :
318318
initRequestBuilder().build());
319-
return Mono.defer(() -> exchangeFunction.exchange(request))
320-
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR);
319+
return Mono.defer(() -> exchangeFunction.exchange(request)
320+
.checkpoint("Request to " + this.httpMethod.name() + " " + this.uri + " [DefaultWebClient]")
321+
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR));
321322
}
322323

323324
private ClientRequest.Builder initRequestBuilder() {
@@ -445,8 +446,8 @@ public <T> Flux<T> bodyToFlux(Class<T> elementType) {
445446

446447
@Override
447448
public <T> Flux<T> bodyToFlux(ParameterizedTypeReference<T> elementType) {
448-
return this.responseMono.flatMapMany(response -> handleBody(response,
449-
response.bodyToFlux(elementType), mono -> mono.flatMapMany(Flux::error)));
449+
return this.responseMono.flatMapMany(response ->
450+
handleBody(response, response.bodyToFlux(elementType), mono -> mono.flatMapMany(Flux::error)));
450451
}
451452

452453
private <T extends Publisher<?>> T handleBody(ClientResponse response,
@@ -459,7 +460,8 @@ private <T extends Publisher<?>> T handleBody(ClientResponse response,
459460
Mono<? extends Throwable> exMono = handler.apply(response, request);
460461
exMono = exMono.flatMap(ex -> drainBody(response, ex));
461462
exMono = exMono.onErrorResume(ex -> drainBody(response, ex));
462-
return errorFunction.apply(exMono);
463+
T result = errorFunction.apply(exMono);
464+
return insertCheckpoint(result, response.statusCode(), request);
463465
}
464466
}
465467
return bodyPublisher;
@@ -477,6 +479,22 @@ private <T> Mono<T> drainBody(ClientResponse response, Throwable ex) {
477479
.onErrorResume(ex2 -> Mono.empty()).thenReturn(ex);
478480
}
479481

482+
@SuppressWarnings("unchecked")
483+
private <T extends Publisher<?>> T insertCheckpoint(T result, HttpStatus status, HttpRequest request) {
484+
String httpMethod = request.getMethodValue();
485+
URI uri = request.getURI();
486+
String description = status + " from " + httpMethod + " " + uri + " [DefaultWebClient]";
487+
if (result instanceof Mono) {
488+
return (T) ((Mono<?>) result).checkpoint(description);
489+
}
490+
else if (result instanceof Flux) {
491+
return (T) ((Flux<?>) result).checkpoint(description);
492+
}
493+
else {
494+
return result;
495+
}
496+
}
497+
480498
private static Mono<WebClientResponseException> createResponseException(
481499
ClientResponse response, HttpRequest request) {
482500

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunctions.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 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.
@@ -105,7 +105,8 @@ public Mono<ClientResponse> exchange(ClientRequest clientRequest) {
105105
.doOnCancel(() -> logger.debug(logPrefix + "Cancel signal (to close connection)"))
106106
.map(httpResponse -> {
107107
logResponse(httpResponse, logPrefix);
108-
return new DefaultClientResponse(httpResponse, this.strategies, logPrefix);
108+
return new DefaultClientResponse(
109+
httpResponse, this.strategies, logPrefix, httpMethod.name() + " " + url);
109110
});
110111
}
111112

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,16 @@ public WebClientResponseException(int statusCode, String statusText,
6363
* Constructor with response data only, and a default message.
6464
* @since 5.1.4
6565
*/
66-
public WebClientResponseException(int statusCode, String statusText,
66+
public WebClientResponseException(int status, String reasonPhrase,
6767
@Nullable HttpHeaders headers, @Nullable byte[] body, @Nullable Charset charset,
6868
@Nullable HttpRequest request) {
6969

70-
this(statusCode + " " + statusText, statusCode, statusText, headers, body, charset, request);
70+
this(initMessage(status, reasonPhrase, request), status, reasonPhrase, headers, body, charset, request);
71+
}
72+
73+
private static String initMessage(int status, String reasonPhrase, @Nullable HttpRequest request) {
74+
return status + " " + reasonPhrase +
75+
(request != null ? " from " + request.getMethodValue() + " " + request.getURI() : "");
7176
}
7277

7378
/**

0 commit comments

Comments
 (0)