Skip to content

Commit b0a8a3e

Browse files
committed
Enhance DisconnectedClientHelper exception type checks
We now look for the target exception types in cause chain as well, but return false if we encounter a RestClient or WebClient exception in the chain. Closes gh-34264
1 parent bb5be21 commit b0a8a3e

File tree

2 files changed

+65
-8
lines changed

2 files changed

+65
-8
lines changed

spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java

+41-8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.util;
1818

19+
import java.util.HashSet;
1920
import java.util.Locale;
2021
import java.util.Set;
2122

@@ -24,28 +25,43 @@
2425

2526
import org.springframework.core.NestedExceptionUtils;
2627
import org.springframework.util.Assert;
28+
import org.springframework.util.ClassUtils;
2729

2830
/**
29-
* Utility methods to assist with identifying and logging exceptions that indicate
30-
* the client has gone away. Such exceptions fill logs with unnecessary stack
31-
* traces. The utility methods help to log a single line message at DEBUG level,
32-
* and a full stacktrace at TRACE level.
31+
* Utility methods to assist with identifying and logging exceptions that
32+
* indicate the server response connection is lost, for example because the
33+
* client has gone away. This class helps to identify such exceptions and
34+
* minimize logging to a single line at DEBUG level, while making the full
35+
* error stacktrace at TRACE level.
3336
*
3437
* @author Rossen Stoyanchev
3538
* @since 6.1
3639
*/
3740
public class DisconnectedClientHelper {
3841

39-
// Look for server response connection issues (aborted), not onward connections
40-
// to other servers (500 errors).
41-
4242
private static final Set<String> EXCEPTION_PHRASES =
4343
Set.of("broken pipe", "connection reset by peer");
4444

4545
private static final Set<String> EXCEPTION_TYPE_NAMES =
4646
Set.of("AbortedException", "ClientAbortException",
4747
"EOFException", "EofException", "AsyncRequestNotUsableException");
4848

49+
private static final Set<Class<?>> CLIENT_EXCEPTION_TYPES = new HashSet<>(2);
50+
51+
static {
52+
try {
53+
ClassLoader classLoader = DisconnectedClientHelper.class.getClassLoader();
54+
CLIENT_EXCEPTION_TYPES.add(ClassUtils.forName(
55+
"org.springframework.web.client.RestClientException", classLoader));
56+
CLIENT_EXCEPTION_TYPES.add(ClassUtils.forName(
57+
"org.springframework.web.reactive.function.client.WebClientException", classLoader));
58+
}
59+
catch (ClassNotFoundException ex) {
60+
// ignore
61+
}
62+
}
63+
64+
4965
private final Log logger;
5066

5167

@@ -85,6 +101,22 @@ else if (logger.isDebugEnabled()) {
85101
* </ul>
86102
*/
87103
public static boolean isClientDisconnectedException(Throwable ex) {
104+
Throwable currentEx = ex;
105+
Throwable lastEx = null;
106+
while (currentEx != null && currentEx != lastEx) {
107+
// Ignore onward connection issues to other servers (500 error)
108+
for (Class<?> exceptionType : CLIENT_EXCEPTION_TYPES) {
109+
if (exceptionType.isInstance(currentEx)) {
110+
return false;
111+
}
112+
}
113+
if (EXCEPTION_TYPE_NAMES.contains(currentEx.getClass().getSimpleName())) {
114+
return true;
115+
}
116+
lastEx = currentEx;
117+
currentEx = currentEx.getCause();
118+
}
119+
88120
String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage();
89121
if (message != null) {
90122
String text = message.toLowerCase(Locale.ROOT);
@@ -94,7 +126,8 @@ public static boolean isClientDisconnectedException(Throwable ex) {
94126
}
95127
}
96128
}
97-
return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName());
129+
130+
return false;
98131
}
99132

100133
}

spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java

+24
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
import org.junit.jupiter.params.provider.ValueSource;
2929
import reactor.netty.channel.AbortedException;
3030

31+
import org.springframework.http.converter.HttpMessageNotReadableException;
32+
import org.springframework.web.client.ResourceAccessException;
3133
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
34+
import org.springframework.web.testfixture.http.MockHttpInputMessage;
3235

3336
import static org.assertj.core.api.Assertions.assertThat;
3437

@@ -66,4 +69,25 @@ static List<Exception> disconnectedExceptions() {
6669
new EOFException(), new EofException(), new AsyncRequestNotUsableException(""));
6770
}
6871

72+
@Test // gh-33064
73+
void nestedDisconnectedException() {
74+
Exception ex = new HttpMessageNotReadableException(
75+
"I/O error while reading input message", new ClientAbortException(),
76+
new MockHttpInputMessage(new byte[0]));
77+
78+
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue();
79+
}
80+
81+
@Test // gh-34264
82+
void onwardClientDisconnectedExceptionPhrase() {
83+
Exception ex = new ResourceAccessException("I/O error", new EOFException("Connection reset by peer"));
84+
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isFalse();
85+
}
86+
87+
@Test
88+
void onwardClientDisconnectedExceptionType() {
89+
Exception ex = new ResourceAccessException("I/O error", new EOFException());
90+
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isFalse();
91+
}
92+
6993
}

0 commit comments

Comments
 (0)