diff --git a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java index 9d49f1b1a30..a62f6312bbb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java @@ -16,6 +16,7 @@ package org.springframework.web.util; +import java.util.HashSet; import java.util.Locale; import java.util.Set; @@ -24,21 +25,20 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.NestedExceptionUtils; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** - * Utility methods to assist with identifying and logging exceptions that indicate - * the client has gone away. Such exceptions fill logs with unnecessary stack - * traces. The utility methods help to log a single line message at DEBUG level, - * and a full stacktrace at TRACE level. + * Utility methods to assist with identifying and logging exceptions that + * indicate the server response connection is lost, for example because the + * client has gone away. This class helps to identify such exceptions and + * minimize logging to a single line at DEBUG level, while making the full + * error stacktrace at TRACE level. * * @author Rossen Stoyanchev * @since 6.1 */ public class DisconnectedClientHelper { - // Look for server response connection issues (aborted), not onward connections - // to other servers (500 errors). - private static final Set EXCEPTION_PHRASES = Set.of("broken pipe", "connection reset by peer"); @@ -46,6 +46,22 @@ public class DisconnectedClientHelper { Set.of("AbortedException", "ClientAbortException", "EOFException", "EofException", "AsyncRequestNotUsableException"); + private static final Set> 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; @@ -85,6 +101,22 @@ public class DisconnectedClientHelper { * */ 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(); if (message != null) { String text = message.toLowerCase(Locale.ROOT); @@ -94,7 +126,8 @@ public class DisconnectedClientHelper { } } } - return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName()); + + return false; } } diff --git a/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java b/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java index 02fbd9a5ab8..296a1920927 100644 --- a/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java +++ b/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 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.testfixture.http.MockHttpInputMessage; import static org.assertj.core.api.Assertions.assertThat; @@ -66,4 +69,25 @@ public class DisconnectedClientHelperTests { 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(); + } + }