Browse Source

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
6.1.x
rstoyanchev 11 months ago
parent
commit
b0a8a3ec5f
  1. 49
      spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java
  2. 24
      spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java

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

@ -16,6 +16,7 @@
package org.springframework.web.util; package org.springframework.web.util;
import java.util.HashSet;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
@ -24,21 +25,20 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.NestedExceptionUtils; import org.springframework.core.NestedExceptionUtils;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/** /**
* Utility methods to assist with identifying and logging exceptions that indicate * Utility methods to assist with identifying and logging exceptions that
* the client has gone away. Such exceptions fill logs with unnecessary stack * indicate the server response connection is lost, for example because the
* traces. The utility methods help to log a single line message at DEBUG level, * client has gone away. This class helps to identify such exceptions and
* and a full stacktrace at TRACE level. * minimize logging to a single line at DEBUG level, while making the full
* error stacktrace at TRACE level.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 6.1 * @since 6.1
*/ */
public class DisconnectedClientHelper { public class DisconnectedClientHelper {
// Look for server response connection issues (aborted), not onward connections
// to other servers (500 errors).
private static final Set<String> EXCEPTION_PHRASES = private static final Set<String> EXCEPTION_PHRASES =
Set.of("broken pipe", "connection reset by peer"); Set.of("broken pipe", "connection reset by peer");
@ -46,6 +46,22 @@ public class DisconnectedClientHelper {
Set.of("AbortedException", "ClientAbortException", Set.of("AbortedException", "ClientAbortException",
"EOFException", "EofException", "AsyncRequestNotUsableException"); "EOFException", "EofException", "AsyncRequestNotUsableException");
private static final Set<Class<?>> CLIENT_EXCEPTION_TYPES = new HashSet<>(2);
static {
try {
ClassLoader classLoader = DisconnectedClientHelper.class.getClassLoader();
CLIENT_EXCEPTION_TYPES.add(ClassUtils.forName(
"org.springframework.web.client.RestClientException", classLoader));
CLIENT_EXCEPTION_TYPES.add(ClassUtils.forName(
"org.springframework.web.reactive.function.client.WebClientException", classLoader));
}
catch (ClassNotFoundException ex) {
// ignore
}
}
private final Log logger; private final Log logger;
@ -85,6 +101,22 @@ public class DisconnectedClientHelper {
* </ul> * </ul>
*/ */
public static boolean isClientDisconnectedException(Throwable ex) { public static boolean isClientDisconnectedException(Throwable ex) {
Throwable currentEx = ex;
Throwable lastEx = null;
while (currentEx != null && currentEx != lastEx) {
// Ignore onward connection issues to other servers (500 error)
for (Class<?> exceptionType : CLIENT_EXCEPTION_TYPES) {
if (exceptionType.isInstance(currentEx)) {
return false;
}
}
if (EXCEPTION_TYPE_NAMES.contains(currentEx.getClass().getSimpleName())) {
return true;
}
lastEx = currentEx;
currentEx = currentEx.getCause();
}
String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage(); String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage();
if (message != null) { if (message != null) {
String text = message.toLowerCase(Locale.ROOT); String text = message.toLowerCase(Locale.ROOT);
@ -94,7 +126,8 @@ public class DisconnectedClientHelper {
} }
} }
} }
return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName());
return false;
} }
} }

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

@ -28,7 +28,10 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import reactor.netty.channel.AbortedException; import reactor.netty.channel.AbortedException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.context.request.async.AsyncRequestNotUsableException; import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import org.springframework.web.testfixture.http.MockHttpInputMessage;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -66,4 +69,25 @@ public class DisconnectedClientHelperTests {
new EOFException(), new EofException(), new AsyncRequestNotUsableException("")); new EOFException(), new EofException(), new AsyncRequestNotUsableException(""));
} }
@Test // gh-33064
void nestedDisconnectedException() {
Exception ex = new HttpMessageNotReadableException(
"I/O error while reading input message", new ClientAbortException(),
new MockHttpInputMessage(new byte[0]));
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue();
}
@Test // gh-34264
void onwardClientDisconnectedExceptionPhrase() {
Exception ex = new ResourceAccessException("I/O error", new EOFException("Connection reset by peer"));
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isFalse();
}
@Test
void onwardClientDisconnectedExceptionType() {
Exception ex = new ResourceAccessException("I/O error", new EOFException());
assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isFalse();
}
} }

Loading…
Cancel
Save