Skip to content

Commit

Permalink
Extra information in WebFlux stacktraces
Browse files Browse the repository at this point in the history
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 gh-22105
  • Loading branch information
rstoyanchev committed Mar 15, 2019
1 parent 495ba2f commit e6d206b
Show file tree
Hide file tree
Showing 16 changed files with 144 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

Expand Down Expand Up @@ -89,6 +90,8 @@ public class HandlerMethod {
@Nullable
private volatile List<Annotation[][]> interfaceParameterAnnotations;

private final String description;


/**
* Create an instance from a bean instance and a method.
Expand All @@ -103,6 +106,7 @@ public HandlerMethod(Object bean, Method method) {
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
this.parameters = initMethodParameters();
evaluateResponseStatus();
this.description = initDescription(this.beanType, this.method);
}

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

/**
Expand All @@ -141,6 +146,7 @@ public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
this.parameters = initMethodParameters();
evaluateResponseStatus();
this.description = initDescription(this.beanType, this.method);
}

/**
Expand All @@ -156,6 +162,7 @@ protected HandlerMethod(HandlerMethod handlerMethod) {
this.parameters = handlerMethod.parameters;
this.responseStatus = handlerMethod.responseStatus;
this.responseStatusReason = handlerMethod.responseStatusReason;
this.description = handlerMethod.description;
this.resolvedFromHandlerMethod = handlerMethod.resolvedFromHandlerMethod;
}

Expand All @@ -174,6 +181,7 @@ private HandlerMethod(HandlerMethod handlerMethod, Object handler) {
this.responseStatus = handlerMethod.responseStatus;
this.responseStatusReason = handlerMethod.responseStatusReason;
this.resolvedFromHandlerMethod = handlerMethod;
this.description = handlerMethod.description;
}

private MethodParameter[] initMethodParameters() {
Expand All @@ -198,6 +206,14 @@ private void evaluateResponseStatus() {
}
}

private static String initDescription(Class<?> beanType, Method method) {
StringJoiner joiner = new StringJoiner(", ", "(", ")");
for (Class<?> paramType : method.getParameterTypes()) {
joiner.add(paramType.getSimpleName());
}
return beanType.getName() + "#" + method.getName() + joiner.toString();
}


/**
* Return the bean for this handler method.
Expand Down Expand Up @@ -389,7 +405,7 @@ public int hashCode() {

@Override
public String toString() {
return this.method.toGenericString();
return this.description;
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
Expand Down Expand Up @@ -53,7 +53,7 @@ public class DefaultWebFilterChain implements WebFilterChain {
private final WebFilter currentFilter;

@Nullable
private final DefaultWebFilterChain next;
private final DefaultWebFilterChain chain;


/**
Expand All @@ -68,7 +68,7 @@ public DefaultWebFilterChain(WebHandler handler, List<WebFilter> filters) {
this.handler = handler;
DefaultWebFilterChain chain = initChain(filters, handler);
this.currentFilter = chain.currentFilter;
this.next = chain.next;
this.chain = chain.chain;
}

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

this.allFilters = allFilters;
this.currentFilter = currentFilter;
this.handler = handler;
this.next = next;
this.chain = chain;
}

/**
Expand Down Expand Up @@ -117,9 +117,14 @@ public WebHandler getHandler() {
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.defer(() ->
this.currentFilter != null && this.next != null ?
this.currentFilter.filter(exchange, this.next) :
this.currentFilter != null && this.chain != null ?
invokeFilter(this.currentFilter, this.chain, exchange) :
this.handler.handle(exchange));
}

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

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
Expand All @@ -22,6 +22,9 @@

import reactor.core.publisher.Mono;

import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import org.springframework.web.server.WebHandler;
Expand All @@ -41,7 +44,10 @@ public class ExceptionHandlingWebHandler extends WebHandlerDecorator {

public ExceptionHandlingWebHandler(WebHandler delegate, List<WebExceptionHandler> handlers) {
super(delegate);
this.exceptionHandlers = Collections.unmodifiableList(new ArrayList<>(handlers));
List<WebExceptionHandler> handlersToUse = new ArrayList<>();
handlersToUse.add(new CheckpointInsertingHandler());
handlersToUse.addAll(handlers);
this.exceptionHandlers = Collections.unmodifiableList(handlersToUse);
}


Expand Down Expand Up @@ -71,4 +77,24 @@ public Mono<Void> handle(ServerWebExchange exchange) {
return completion;
}


/**
* WebExceptionHandler to insert a checkpoint with current URL information.
* Must be the first in order to ensure we catch the error signal before
* the exception is handled and e.g. turned into an error response.
* @since 5.2
*/
private static class CheckpointInsertingHandler implements WebExceptionHandler {

@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpRequest request = exchange.getRequest();
String rawQuery = request.getURI().getRawQuery();
String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : "";
HttpMethod httpMethod = request.getMethod();
String description = "HTTP " + httpMethod + " \"" + request.getPath() + query + "\"";
return Mono.error(ex).checkpoint(description + " [ExceptionHandlingWebHandler]").cast(Void.class);
}
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
Expand Down Expand Up @@ -67,11 +67,6 @@
*/
public class DispatcherHandler implements WebHandler, ApplicationContextAware {

@SuppressWarnings("ThrowableInstanceNeverThrown")
private static final Exception HANDLER_NOT_FOUND_EXCEPTION =
new ResponseStatusException(HttpStatus.NOT_FOUND, "No matching handler");


@Nullable
private List<HandlerMapping> handlerMappings;

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

private Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
return getResultHandler(result).handleResult(exchange, result)
.onErrorResume(ex -> result.applyExceptionHandler(ex).flatMap(exceptionResult ->
getResultHandler(exceptionResult).handleResult(exchange, exceptionResult)));
.checkpoint("Handler " + result.getHandler() + " [DispatcherHandler]")
.onErrorResume(ex ->
result.applyExceptionHandler(ex).flatMap(exResult -> {
String text = "Exception handler " + exResult.getHandler() +
", error=\"" + ex.getMessage() + "\" [DispatcherHandler]";
return getResultHandler(exResult).handleResult(exchange, exResult).checkpoint(text);
}));
}

private HandlerResultHandler getResultHandler(HandlerResult handlerResult) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
Expand Down Expand Up @@ -56,12 +56,17 @@ class DefaultClientResponse implements ClientResponse {

private final String logPrefix;

private final String requestDescription;


public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies,
String logPrefix, String requestDescription) {

public DefaultClientResponse(ClientHttpResponse response, ExchangeStrategies strategies, String logPrefix) {
this.response = response;
this.strategies = strategies;
this.headers = new DefaultHeaders();
this.logPrefix = logPrefix;
this.requestDescription = requestDescription;
}


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

@SuppressWarnings("unchecked")
@Override
public <T> T body(BodyExtractor<T, ? super ClientHttpResponse> extractor) {
return extractor.extract(this.response, new BodyExtractor.Context() {
T result = extractor.extract(this.response, new BodyExtractor.Context() {
@Override
public List<HttpMessageReader<?>> messageReaders() {
return strategies.messageReaders();
}

@Override
public Optional<ServerHttpResponse> serverResponse() {
return Optional.empty();
}

@Override
public Map<String, Object> hints() {
return Hints.from(Hints.LOG_PREFIX_HINT, logPrefix);
}
});
String description = "Body from " + this.requestDescription + " [DefaultClientResponse]";
if (result instanceof Mono) {
return (T) ((Mono<?>) result).checkpoint(description);
}
else if (result instanceof Flux) {
return (T) ((Flux<?>) result).checkpoint(description);
}
else {
return result;
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
Expand Down Expand Up @@ -136,7 +136,7 @@ public ClientResponse build() {
// When building ClientResponse manually, the ClientRequest.logPrefix() has to be passed,
// e.g. via ClientResponse.Builder, but this (builder) is not used currently.

return new DefaultClientResponse(httpResponse, this.strategies, "");
return new DefaultClientResponse(httpResponse, this.strategies, "", "");
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,9 @@ public Mono<ClientResponse> exchange() {
ClientRequest request = (this.inserter != null ?
initRequestBuilder().body(this.inserter).build() :
initRequestBuilder().build());
return Mono.defer(() -> exchangeFunction.exchange(request))
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR);
return Mono.defer(() -> exchangeFunction.exchange(request)
.checkpoint("Request to " + this.httpMethod.name() + " " + this.uri + " [DefaultWebClient]")
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR));
}

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

@Override
public <T> Flux<T> bodyToFlux(ParameterizedTypeReference<T> elementType) {
return this.responseMono.flatMapMany(response -> handleBody(response,
response.bodyToFlux(elementType), mono -> mono.flatMapMany(Flux::error)));
return this.responseMono.flatMapMany(response ->
handleBody(response, response.bodyToFlux(elementType), mono -> mono.flatMapMany(Flux::error)));
}

private <T extends Publisher<?>> T handleBody(ClientResponse response,
Expand All @@ -459,7 +460,8 @@ private <T extends Publisher<?>> T handleBody(ClientResponse response,
Mono<? extends Throwable> exMono = handler.apply(response, request);
exMono = exMono.flatMap(ex -> drainBody(response, ex));
exMono = exMono.onErrorResume(ex -> drainBody(response, ex));
return errorFunction.apply(exMono);
T result = errorFunction.apply(exMono);
return insertCheckpoint(result, response.statusCode(), request);
}
}
return bodyPublisher;
Expand All @@ -477,6 +479,22 @@ private <T> Mono<T> drainBody(ClientResponse response, Throwable ex) {
.onErrorResume(ex2 -> Mono.empty()).thenReturn(ex);
}

@SuppressWarnings("unchecked")
private <T extends Publisher<?>> T insertCheckpoint(T result, HttpStatus status, HttpRequest request) {
String httpMethod = request.getMethodValue();
URI uri = request.getURI();
String description = status + " from " + httpMethod + " " + uri + " [DefaultWebClient]";
if (result instanceof Mono) {
return (T) ((Mono<?>) result).checkpoint(description);
}
else if (result instanceof Flux) {
return (T) ((Flux<?>) result).checkpoint(description);
}
else {
return result;
}
}

private static Mono<WebClientResponseException> createResponseException(
ClientResponse response, HttpRequest request) {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
Expand Down Expand Up @@ -105,7 +105,8 @@ public Mono<ClientResponse> exchange(ClientRequest clientRequest) {
.doOnCancel(() -> logger.debug(logPrefix + "Cancel signal (to close connection)"))
.map(httpResponse -> {
logResponse(httpResponse, logPrefix);
return new DefaultClientResponse(httpResponse, this.strategies, logPrefix);
return new DefaultClientResponse(
httpResponse, this.strategies, logPrefix, httpMethod.name() + " " + url);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,16 @@ public WebClientResponseException(int statusCode, String statusText,
* Constructor with response data only, and a default message.
* @since 5.1.4
*/
public WebClientResponseException(int statusCode, String statusText,
public WebClientResponseException(int status, String reasonPhrase,
@Nullable HttpHeaders headers, @Nullable byte[] body, @Nullable Charset charset,
@Nullable HttpRequest request) {

this(statusCode + " " + statusText, statusCode, statusText, headers, body, charset, request);
this(initMessage(status, reasonPhrase, request), status, reasonPhrase, headers, body, charset, request);
}

private static String initMessage(int status, String reasonPhrase, @Nullable HttpRequest request) {
return status + " " + reasonPhrase +
(request != null ? " from " + request.getMethodValue() + " " + request.getURI() : "");
}

/**
Expand Down
Loading

0 comments on commit e6d206b

Please sign in to comment.