diff --git a/spring-web/src/main/java/org/springframework/web/client/NoOpResponseErrorHandler.java b/spring-web/src/main/java/org/springframework/web/client/NoOpResponseErrorHandler.java
index 4d1498b4030..ae699f2bc5c 100644
--- a/spring-web/src/main/java/org/springframework/web/client/NoOpResponseErrorHandler.java
+++ b/spring-web/src/main/java/org/springframework/web/client/NoOpResponseErrorHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,7 +25,7 @@ import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler;
/**
* A basic, no operation {@link ResponseErrorHandler} implementation suitable
- * for ignoring any error using the {@link RestTemplate}.
+ * for ignoring any error using the {@link RestTemplate} or {@link RestClient}.
*
This implementation is not suitable with the {@link RestClient} as it uses
* a list of candidates where the first matching is invoked. If you want to
* disable default status handlers with the {@code RestClient}, consider
diff --git a/spring-web/src/main/java/org/springframework/web/client/ResponseErrorHandler.java b/spring-web/src/main/java/org/springframework/web/client/ResponseErrorHandler.java
index 088b4138242..61801317140 100644
--- a/spring-web/src/main/java/org/springframework/web/client/ResponseErrorHandler.java
+++ b/spring-web/src/main/java/org/springframework/web/client/ResponseErrorHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,8 +23,11 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpResponse;
/**
- * Strategy interface used by the {@link RestTemplate} to determine
- * whether a particular response has an error or not.
+ * Strategy interface used by the {@link RestTemplate} and {@link RestClient} to
+ * determine whether a particular response has an error or not.
+ *
+ *
Note that {@code RestClient} also supports and recommends use of
+ * {@link RestClient.ResponseSpec#onStatus status handlers}.
*
* @author Arjen Poutsma
* @since 3.0
diff --git a/spring-web/src/main/java/org/springframework/web/client/RestClientException.java b/spring-web/src/main/java/org/springframework/web/client/RestClientException.java
index f75de850337..b849acaa2be 100644
--- a/spring-web/src/main/java/org/springframework/web/client/RestClientException.java
+++ b/spring-web/src/main/java/org/springframework/web/client/RestClientException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,14 +19,16 @@ package org.springframework.web.client;
import org.jspecify.annotations.Nullable;
import org.springframework.core.NestedRuntimeException;
-import org.springframework.http.client.ClientHttpResponse;
/**
- * Base class for exceptions thrown by {@link RestTemplate} in case a request
- * fails because of a server error response, as determined via
- * {@link ResponseErrorHandler#hasError(ClientHttpResponse)}, failure to decode
+ * Base class for exceptions thrown by {@link RestClient} and {@link RestTemplate}
+ * in case a request fails because of a server error response, a failure to decode
* the response, or a low level I/O error.
*
+ *
Server error responses are determined by
+ * {@link RestClient.ResponseSpec#onStatus status handlers} for {@code RestClient},
+ * and by {@link ResponseErrorHandler} for {@code RestTemplate}.
+ *
* @author Arjen Poutsma
* @since 3.0
*/
diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
index 4c32c8bdaf7..22f5fdacf59 100644
--- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
+++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
@@ -178,7 +178,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
/**
- * Create a new instance of the {@link RestTemplate} using default settings.
+ * Create a new instance with default settings.
* Default {@link HttpMessageConverter HttpMessageConverters} are initialized.
*/
public RestTemplate() {
@@ -237,7 +237,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
}
/**
- * Create a new instance of the {@link RestTemplate} based on the given {@link ClientHttpRequestFactory}.
+ * Create a new instance with the given {@link ClientHttpRequestFactory}.
* @param requestFactory the HTTP request factory to use
* @see org.springframework.http.client.SimpleClientHttpRequestFactory
* @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory
@@ -248,9 +248,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
}
/**
- * Create a new instance of the {@link RestTemplate} using the given list of
- * {@link HttpMessageConverter} to use.
- * @param messageConverters the list of {@link HttpMessageConverter} to use
+ * Create a new instance with the given message converters.
+ * @param messageConverters the list of converters to use
* @since 3.2.7
*/
public RestTemplate(List> messageConverters) {
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 b2b16f135ad..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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.springframework.web.util;
+import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
@@ -24,12 +25,14 @@ 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
@@ -37,12 +40,28 @@ import org.springframework.util.Assert;
public class DisconnectedClientHelper {
private static final Set EXCEPTION_PHRASES =
- Set.of("broken pipe", "connection reset");
+ Set.of("broken pipe", "connection reset by peer");
private static final Set EXCEPTION_TYPE_NAMES =
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;
@@ -79,10 +98,25 @@ public class DisconnectedClientHelper {
* ClientAbortException or EOFException for Tomcat
* EofException for Jetty
* IOException "Broken pipe" or "connection reset by peer"
- * SocketException "Connection reset"
*
*/
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);
@@ -92,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
new file mode 100644
index 00000000000..296a1920927
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/util/DisconnectedClientHelperTests.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.web.util;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.catalina.connector.ClientAbortException;
+import org.eclipse.jetty.io.EofException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+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;
+
+/**
+ * Unit tests for {@link DisconnectedClientHelper}.
+ * @author Rossen Stoyanchev
+ */
+public class DisconnectedClientHelperTests {
+
+ @ParameterizedTest
+ @ValueSource(strings = {"broKen pipe", "connection reset By peer"})
+ void exceptionPhrases(String phrase) {
+ Exception ex = new IOException(phrase);
+ assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue();
+
+ ex = new IOException(ex);
+ assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue();
+ }
+
+ @Test
+ void connectionResetExcluded() {
+ Exception ex = new IOException("connection reset");
+ assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isFalse();
+ }
+
+ @ParameterizedTest
+ @MethodSource("disconnectedExceptions")
+ void name(Exception ex) {
+ assertThat(DisconnectedClientHelper.isClientDisconnectedException(ex)).isTrue();
+ }
+
+ static List disconnectedExceptions() {
+ return List.of(
+ new AbortedException(""), new ClientAbortException(""),
+ 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();
+ }
+
+}