Skip to content

Commit 9d8e9cf

Browse files
committed
Common root cause introspection algorithm in NestedExceptionUtils
Issue: SPR-15510
1 parent 16901b1 commit 9d8e9cf

File tree

5 files changed

+97
-70
lines changed

5 files changed

+97
-70
lines changed

spring-core/src/main/java/org/springframework/core/NestedCheckedException.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2017 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.
@@ -80,13 +80,7 @@ public String getMessage() {
8080
* @return the innermost exception, or {@code null} if none
8181
*/
8282
public Throwable getRootCause() {
83-
Throwable rootCause = null;
84-
Throwable cause = getCause();
85-
while (cause != null && cause != rootCause) {
86-
rootCause = cause;
87-
cause = cause.getCause();
88-
}
89-
return rootCause;
83+
return NestedExceptionUtils.getRootCause(this);
9084
}
9185

9286
/**

spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2008 the original author or authors.
2+
* Copyright 2002-2017 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.
@@ -39,17 +39,48 @@ public abstract class NestedExceptionUtils {
3939
* @return the full exception message
4040
*/
4141
public static String buildMessage(String message, Throwable cause) {
42-
if (cause != null) {
43-
StringBuilder sb = new StringBuilder();
44-
if (message != null) {
45-
sb.append(message).append("; ");
46-
}
47-
sb.append("nested exception is ").append(cause);
48-
return sb.toString();
49-
}
50-
else {
42+
if (cause == null) {
5143
return message;
5244
}
45+
StringBuilder sb = new StringBuilder(64);
46+
if (message != null) {
47+
sb.append(message).append("; ");
48+
}
49+
sb.append("nested exception is ").append(cause);
50+
return sb.toString();
51+
}
52+
53+
/**
54+
* Retrieve the innermost cause of the given exception, if any.
55+
* @param original the original exception to introspect
56+
* @return the innermost exception, or {@code null} if none
57+
* @since 4.3.9
58+
*/
59+
public static Throwable getRootCause(Throwable original) {
60+
if (original == null) {
61+
return null;
62+
}
63+
Throwable rootCause = null;
64+
Throwable cause = original.getCause();
65+
while (cause != null && cause != rootCause) {
66+
rootCause = cause;
67+
cause = cause.getCause();
68+
}
69+
return rootCause;
70+
}
71+
72+
/**
73+
* Retrieve the most specific cause of the given exception, that is,
74+
* either the innermost cause (root cause) or the exception itself.
75+
* <p>Differs from {@link #getRootCause} in that it falls back
76+
* to the original exception if there is no root cause.
77+
* @param original the original exception to introspect
78+
* @return the most specific cause (never {@code null})
79+
* @since 4.3.9
80+
*/
81+
public static Throwable getMostSpecificCause(Throwable original) {
82+
Throwable rootCause = getRootCause(original);
83+
return (rootCause != null ? rootCause : original);
5384
}
5485

5586
}

spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2017 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.
@@ -81,13 +81,7 @@ public String getMessage() {
8181
* @since 2.0
8282
*/
8383
public Throwable getRootCause() {
84-
Throwable rootCause = null;
85-
Throwable cause = getCause();
86-
while (cause != null && cause != rootCause) {
87-
rootCause = cause;
88-
cause = cause.getCause();
89-
}
90-
return rootCause;
84+
return NestedExceptionUtils.getRootCause(this);
9185
}
9286

9387
/**

spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import org.apache.commons.logging.LogFactory;
2525
import reactor.core.publisher.Mono;
2626

27-
import org.springframework.core.NestedCheckedException;
27+
import org.springframework.core.NestedExceptionUtils;
2828
import org.springframework.http.HttpStatus;
2929
import org.springframework.http.codec.ServerCodecConfigurer;
3030
import org.springframework.http.server.reactive.HttpHandler;
@@ -33,7 +33,6 @@
3333
import org.springframework.util.Assert;
3434
import org.springframework.web.server.ServerWebExchange;
3535
import org.springframework.web.server.WebHandler;
36-
import org.springframework.web.server.handler.ExceptionHandlingWebHandler;
3736
import org.springframework.web.server.handler.WebHandlerDecorator;
3837
import org.springframework.web.server.session.DefaultWebSessionManager;
3938
import org.springframework.web.server.session.WebSessionManager;
@@ -60,8 +59,17 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
6059
* or a full stack trace only at TRACE level.
6160
*/
6261
private static final String DISCONNECTED_CLIENT_LOG_CATEGORY =
63-
ExceptionHandlingWebHandler.class.getName() + ".DisconnectedClient";
62+
"org.springframework.web.server.DisconnectedClient";
6463

64+
/**
65+
* Tomcat: ClientAbortException or EOFException
66+
* Jetty: EofException
67+
* WildFly, GlassFish: java.io.IOException "Broken pipe" (already covered)
68+
* <p>TODO:
69+
* This definition is currently duplicated between HttpWebHandlerAdapter
70+
* and AbstractSockJsSession. It is a candidate for a common utility class.
71+
* @see #indicatesDisconnectedClient(Throwable)
72+
*/
6573
private static final Set<String> DISCONNECTED_CLIENT_EXCEPTIONS =
6674
new HashSet<>(Arrays.asList("ClientAbortException", "EOFException", "EofException"));
6775

@@ -115,7 +123,7 @@ public void setCodecConfigurer(ServerCodecConfigurer codecConfigurer) {
115123
* Return the configured {@link ServerCodecConfigurer}.
116124
*/
117125
public ServerCodecConfigurer getCodecConfigurer() {
118-
return this.codecConfigurer != null ? this.codecConfigurer : ServerCodecConfigurer.create();
126+
return (this.codecConfigurer != null ? this.codecConfigurer : ServerCodecConfigurer.create());
119127
}
120128

121129

@@ -125,7 +133,7 @@ public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response)
125133
return getDelegate().handle(exchange)
126134
.onErrorResume(ex -> {
127135
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
128-
logException(ex);
136+
logHandleFailure(ex);
129137
return Mono.empty();
130138
})
131139
.then(Mono.defer(response::setComplete));
@@ -135,25 +143,25 @@ protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttp
135143
return new DefaultServerWebExchange(request, response, this.sessionManager, getCodecConfigurer());
136144
}
137145

138-
@SuppressWarnings("serial")
139-
private void logException(Throwable ex) {
140-
NestedCheckedException nestedEx = new NestedCheckedException("", ex) {};
141-
if ("Broken pipe".equalsIgnoreCase(nestedEx.getMostSpecificCause().getMessage()) ||
142-
DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName())) {
143-
146+
private void logHandleFailure(Throwable ex) {
147+
if (indicatesDisconnectedClient(ex)) {
144148
if (disconnectedClientLogger.isTraceEnabled()) {
145149
disconnectedClientLogger.trace("Looks like the client has gone away", ex);
146150
}
147151
else if (disconnectedClientLogger.isDebugEnabled()) {
148-
disconnectedClientLogger.debug(
149-
"The client has gone away: " + nestedEx.getMessage() +
150-
" (For a full stack trace, set the log category" +
151-
"'" + DISCONNECTED_CLIENT_LOG_CATEGORY + "' to TRACE)");
152+
disconnectedClientLogger.debug("Looks like the client has gone away: " + ex +
153+
" (For a full stack trace, set the log category '" + DISCONNECTED_CLIENT_LOG_CATEGORY +
154+
"' to TRACE level.)");
152155
}
153156
}
154157
else {
155-
logger.error("Could not complete request", ex);
158+
logger.error("Failed to handle request", ex);
156159
}
157160
}
158161

162+
private boolean indicatesDisconnectedClient(Throwable ex) {
163+
return ("Broken pipe".equalsIgnoreCase(NestedExceptionUtils.getMostSpecificCause(ex).getMessage()) ||
164+
DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName()));
165+
}
166+
159167
}

spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.io.IOException;
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
22-
import java.util.Collections;
2322
import java.util.Date;
2423
import java.util.HashSet;
2524
import java.util.List;
@@ -31,7 +30,7 @@
3130
import org.apache.commons.logging.Log;
3231
import org.apache.commons.logging.LogFactory;
3332

34-
import org.springframework.core.NestedCheckedException;
33+
import org.springframework.core.NestedExceptionUtils;
3534
import org.springframework.util.Assert;
3635
import org.springframework.web.socket.CloseStatus;
3736
import org.springframework.web.socket.TextMessage;
@@ -70,25 +69,25 @@ private enum State {NEW, OPEN, CLOSED}
7069
public static final String DISCONNECTED_CLIENT_LOG_CATEGORY =
7170
"org.springframework.web.socket.sockjs.DisconnectedClient";
7271

72+
/**
73+
* Tomcat: ClientAbortException or EOFException
74+
* Jetty: EofException
75+
* WildFly, GlassFish: java.io.IOException "Broken pipe" (already covered)
76+
* <p>TODO:
77+
* This definition is currently duplicated between HttpWebHandlerAdapter
78+
* and AbstractSockJsSession. It is a candidate for a common utility class.
79+
* @see #indicatesDisconnectedClient(Throwable)
80+
*/
81+
private static final Set<String> DISCONNECTED_CLIENT_EXCEPTIONS =
82+
new HashSet<>(Arrays.asList("ClientAbortException", "EOFException", "EofException"));
83+
84+
7385
/**
7486
* Separate logger to use on network IO failure after a client has gone away.
7587
* @see #DISCONNECTED_CLIENT_LOG_CATEGORY
7688
*/
7789
protected static final Log disconnectedClientLogger = LogFactory.getLog(DISCONNECTED_CLIENT_LOG_CATEGORY);
7890

79-
80-
private static final Set<String> disconnectedClientExceptions;
81-
82-
static {
83-
Set<String> set = new HashSet<String>(4);
84-
set.add("ClientAbortException"); // Tomcat
85-
set.add("EOFException"); // Tomcat
86-
set.add("EofException"); // Jetty
87-
// java.io.IOException "Broken pipe" on WildFly, Glassfish (already covered)
88-
disconnectedClientExceptions = Collections.unmodifiableSet(set);
89-
}
90-
91-
9291
protected final Log logger = LogFactory.getLog(getClass());
9392

9493
protected final Object responseLock = new Object();
@@ -340,28 +339,28 @@ protected void writeFrame(SockJsFrame frame) throws SockJsTransportFailureExcept
340339
}
341340
}
342341

343-
private void logWriteFrameFailure(Throwable failure) {
344-
@SuppressWarnings("serial")
345-
NestedCheckedException nestedException = new NestedCheckedException("", failure) {};
346-
347-
if ("Broken pipe".equalsIgnoreCase(nestedException.getMostSpecificCause().getMessage()) ||
348-
disconnectedClientExceptions.contains(failure.getClass().getSimpleName())) {
342+
protected abstract void writeFrameInternal(SockJsFrame frame) throws IOException;
349343

344+
private void logWriteFrameFailure(Throwable ex) {
345+
if (indicatesDisconnectedClient(ex)) {
350346
if (disconnectedClientLogger.isTraceEnabled()) {
351-
disconnectedClientLogger.trace("Looks like the client has gone away", failure);
347+
disconnectedClientLogger.trace("Looks like the client has gone away", ex);
352348
}
353349
else if (disconnectedClientLogger.isDebugEnabled()) {
354-
disconnectedClientLogger.debug("Looks like the client has gone away: " +
355-
nestedException.getMessage() + " (For full stack trace, set the '" +
356-
DISCONNECTED_CLIENT_LOG_CATEGORY + "' log category to TRACE level)");
350+
disconnectedClientLogger.debug("Looks like the client has gone away: " + ex +
351+
" (For a full stack trace, set the log category '" + DISCONNECTED_CLIENT_LOG_CATEGORY +
352+
"' to TRACE level.)");
357353
}
358354
}
359355
else {
360-
logger.debug("Terminating connection after failure to send message to client", failure);
356+
logger.debug("Terminating connection after failure to send message to client", ex);
361357
}
362358
}
363359

364-
protected abstract void writeFrameInternal(SockJsFrame frame) throws IOException;
360+
private boolean indicatesDisconnectedClient(Throwable ex) {
361+
return ("Broken pipe".equalsIgnoreCase(NestedExceptionUtils.getMostSpecificCause(ex).getMessage()) ||
362+
DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName()));
363+
}
365364

366365

367366
// Delegation methods
@@ -421,7 +420,8 @@ public void tryCloseWithSockJsTransportError(Throwable error, CloseStatus closeS
421420
delegateError(error);
422421
}
423422
catch (Throwable delegateException) {
424-
// ignore
423+
// Ignore
424+
logger.debug("Exception from error handling delegate", delegateException);
425425
}
426426
try {
427427
close(closeStatus);

0 commit comments

Comments
 (0)