Merge branch '6.2.x'
This commit is contained in:
@@ -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}.
|
||||
* <p>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
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Note that {@code RestClient} also supports and recommends use of
|
||||
* {@link RestClient.ResponseSpec#onStatus status handlers}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 3.0
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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
|
||||
*/
|
||||
|
||||
@@ -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<HttpMessageConverter<?>> messageConverters) {
|
||||
|
||||
@@ -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<String> EXCEPTION_PHRASES =
|
||||
Set.of("broken pipe", "connection reset");
|
||||
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;
|
||||
|
||||
|
||||
@@ -79,10 +98,25 @@ public class DisconnectedClientHelper {
|
||||
* <li>ClientAbortException or EOFException for Tomcat
|
||||
* <li>EofException for Jetty
|
||||
* <li>IOException "Broken pipe" or "connection reset by peer"
|
||||
* <li>SocketException "Connection reset"
|
||||
* </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);
|
||||
@@ -92,7 +126,8 @@ public class DisconnectedClientHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Exception> 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();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user