|
1 | 1 | /*
|
2 |
| - * Copyright 2012-2018 the original author or authors. |
| 2 | + * Copyright 2012-2019 the original author or authors. |
3 | 3 | *
|
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | 5 | * you may not use this file except in compliance with the License.
|
|
16 | 16 |
|
17 | 17 | package org.springframework.boot.autoconfigure.web.reactive.error;
|
18 | 18 |
|
| 19 | +import java.util.Arrays; |
19 | 20 | import java.util.Collections;
|
20 | 21 | import java.util.Date;
|
| 22 | +import java.util.HashSet; |
21 | 23 | import java.util.List;
|
22 | 24 | import java.util.Map;
|
| 25 | +import java.util.Set; |
23 | 26 |
|
| 27 | +import org.apache.commons.logging.Log; |
24 | 28 | import reactor.core.publisher.Mono;
|
25 | 29 |
|
26 | 30 | import org.springframework.beans.factory.InitializingBean;
|
|
29 | 33 | import org.springframework.boot.web.reactive.error.ErrorAttributes;
|
30 | 34 | import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
|
31 | 35 | import org.springframework.context.ApplicationContext;
|
| 36 | +import org.springframework.core.NestedExceptionUtils; |
32 | 37 | import org.springframework.core.io.Resource;
|
| 38 | +import org.springframework.http.HttpLogging; |
| 39 | +import org.springframework.http.HttpStatus; |
33 | 40 | import org.springframework.http.codec.HttpMessageReader;
|
34 | 41 | import org.springframework.http.codec.HttpMessageWriter;
|
35 | 42 | import org.springframework.util.Assert;
|
36 | 43 | import org.springframework.util.CollectionUtils;
|
| 44 | +import org.springframework.util.StringUtils; |
37 | 45 | import org.springframework.web.reactive.function.BodyInserters;
|
38 | 46 | import org.springframework.web.reactive.function.server.RouterFunction;
|
39 | 47 | import org.springframework.web.reactive.function.server.ServerRequest;
|
|
52 | 60 | public abstract class AbstractErrorWebExceptionHandler
|
53 | 61 | implements ErrorWebExceptionHandler, InitializingBean {
|
54 | 62 |
|
| 63 | + /** |
| 64 | + * Currently duplicated from Spring WebFlux HttpWebHandlerAdapter. |
| 65 | + */ |
| 66 | + private static final Set<String> DISCONNECTED_CLIENT_EXCEPTIONS = new HashSet<>( |
| 67 | + Arrays.asList("ClientAbortException", "EOFException", "EofException")); |
| 68 | + |
| 69 | + private static final Log logger = HttpLogging |
| 70 | + .forLogName(AbstractErrorWebExceptionHandler.class); |
| 71 | + |
55 | 72 | private final ApplicationContext applicationContext;
|
56 | 73 |
|
57 | 74 | private final ErrorAttributes errorAttributes;
|
@@ -236,17 +253,51 @@ protected abstract RouterFunction<ServerResponse> getRoutingFunction(
|
236 | 253 |
|
237 | 254 | @Override
|
238 | 255 | public Mono<Void> handle(ServerWebExchange exchange, Throwable throwable) {
|
239 |
| - if (exchange.getResponse().isCommitted()) { |
| 256 | + if (exchange.getResponse().isCommitted() |
| 257 | + || isDisconnectedClientError(throwable)) { |
240 | 258 | return Mono.error(throwable);
|
241 | 259 | }
|
242 | 260 | this.errorAttributes.storeErrorInformation(throwable, exchange);
|
243 | 261 | ServerRequest request = ServerRequest.create(exchange, this.messageReaders);
|
244 | 262 | return getRoutingFunction(this.errorAttributes).route(request)
|
245 | 263 | .switchIfEmpty(Mono.error(throwable))
|
246 | 264 | .flatMap((handler) -> handler.handle(request))
|
| 265 | + .doOnNext((response) -> logError(request, response, throwable)) |
247 | 266 | .flatMap((response) -> write(exchange, response));
|
248 | 267 | }
|
249 | 268 |
|
| 269 | + private boolean isDisconnectedClientError(Throwable ex) { |
| 270 | + String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage(); |
| 271 | + message = (message != null) ? message.toLowerCase() : ""; |
| 272 | + String className = ex.getClass().getSimpleName(); |
| 273 | + return (message.contains("broken pipe") |
| 274 | + || DISCONNECTED_CLIENT_EXCEPTIONS.contains(className)); |
| 275 | + } |
| 276 | + |
| 277 | + private void logError(ServerRequest request, ServerResponse response, |
| 278 | + Throwable throwable) { |
| 279 | + if (logger.isDebugEnabled()) { |
| 280 | + logger.debug( |
| 281 | + request.exchange().getLogPrefix() + formatError(throwable, request)); |
| 282 | + } |
| 283 | + if (response.statusCode().equals(HttpStatus.INTERNAL_SERVER_ERROR)) { |
| 284 | + logger.error(request.exchange().getLogPrefix() + "500 Server Error for " |
| 285 | + + formatRequest(request), throwable); |
| 286 | + } |
| 287 | + } |
| 288 | + |
| 289 | + private String formatError(Throwable ex, ServerRequest request) { |
| 290 | + String reason = ex.getClass().getSimpleName() + ": " + ex.getMessage(); |
| 291 | + return "Resolved [" + reason + "] for HTTP " + request.methodName() + " " |
| 292 | + + request.path(); |
| 293 | + } |
| 294 | + |
| 295 | + private String formatRequest(ServerRequest request) { |
| 296 | + String rawQuery = request.uri().getRawQuery(); |
| 297 | + String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : ""; |
| 298 | + return "HTTP " + request.methodName() + " \"" + request.path() + query + "\""; |
| 299 | + } |
| 300 | + |
250 | 301 | private Mono<? extends Void> write(ServerWebExchange exchange,
|
251 | 302 | ServerResponse response) {
|
252 | 303 | // force content-type since writeTo won't overwrite response header values
|
|
0 commit comments