Skip to content

Commit

Permalink
Enhance DisconnectedClientHelper exception type checks
Browse files Browse the repository at this point in the history
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 spring-projectsgh-34264
  • Loading branch information
rstoyanchev committed Jan 21, 2025
1 parent bb5be21 commit b0a8a3e
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.springframework.web.util;

import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

Expand All @@ -24,28 +25,43 @@

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<String> EXCEPTION_PHRASES =
Set.of("broken pipe", "connection reset by peer");

private static final Set<String> EXCEPTION_TYPE_NAMES =
Set.of("AbortedException", "ClientAbortException",
"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;


Expand Down Expand Up @@ -85,6 +101,22 @@ else if (logger.isDebugEnabled()) {
* </ul>
*/
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);
Expand All @@ -94,7 +126,8 @@ public static boolean isClientDisconnectedException(Throwable ex) {
}
}
}
return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName());

return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
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;

Expand Down Expand Up @@ -66,4 +69,25 @@ static List<Exception> disconnectedExceptions() {
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();
}

}

0 comments on commit b0a8a3e

Please sign in to comment.