diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java
index 7d15d392f0..2c08cc78bb 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -28,11 +28,12 @@ import javax.servlet.AsyncListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.context.request.ServletWebRequest;
/**
- * A Servlet 3.0 implementation of {@link AsyncWebRequest}.
+ * A Servlet implementation of {@link AsyncWebRequest}.
*
*
The servlet and all filters involved in an async request must have async
* support enabled using the Servlet API or by adding an
@@ -44,11 +45,7 @@ import org.springframework.web.context.request.ServletWebRequest;
*/
public class StandardServletAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest, AsyncListener {
- private Long timeout;
-
- private AsyncContext asyncContext;
-
- private AtomicBoolean asyncCompleted = new AtomicBoolean();
+ private final AtomicBoolean asyncCompleted = new AtomicBoolean();
private final List timeoutHandlers = new ArrayList<>();
@@ -56,6 +53,12 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
private final List completionHandlers = new ArrayList<>();
+ @Nullable
+ private Long timeout;
+
+ @Nullable
+ private AsyncContext asyncContext;
+
/**
* Create a new instance for the given request/response pair.
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java
index 9425b197e7..998be73e57 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java
@@ -22,7 +22,6 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
-import java.util.concurrent.RejectedExecutionException;
import javax.servlet.http.HttpServletRequest;
@@ -36,6 +35,7 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.async.DeferredResult.DeferredResultHandler;
+import org.springframework.web.util.DisconnectedClientHelper;
/**
* The central class for managing asynchronous request processing, mainly intended
@@ -69,6 +69,16 @@ public final class WebAsyncManager {
private static final Log logger = LogFactory.getLog(WebAsyncManager.class);
+ /**
+ * Log category to use for network failure after a client has gone away.
+ * @see DisconnectedClientHelper
+ */
+ private static final String DISCONNECTED_CLIENT_LOG_CATEGORY =
+ "org.springframework.web.server.DisconnectedClient";
+
+ private static final DisconnectedClientHelper disconnectedClientHelper =
+ new DisconnectedClientHelper(DISCONNECTED_CLIENT_LOG_CATEGORY);
+
private static final CallableProcessingInterceptor timeoutCallableInterceptor =
new TimeoutCallableProcessingInterceptor();
@@ -351,10 +361,9 @@ public final class WebAsyncManager {
});
interceptorChain.setTaskFuture(future);
}
- catch (RejectedExecutionException ex) {
+ catch (Throwable ex) {
Object result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, ex);
setConcurrentResultAndDispatch(result);
- throw ex;
}
}
@@ -395,9 +404,14 @@ public final class WebAsyncManager {
return;
}
+ if (result instanceof Exception) {
+ if (disconnectedClientHelper.checkAndLogClientDisconnectedException((Exception) result)) {
+ return;
+ }
+ }
+
if (logger.isDebugEnabled()) {
- boolean isError = result instanceof Throwable;
- logger.debug("Async " + (isError ? "error" : "result set") +
+ logger.debug("Async " + (this.errorHandlingInProgress ? "error" : "result set") +
", dispatch to " + formatUri(this.asyncWebRequest));
}
this.asyncWebRequest.dispatch();
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
new file mode 100644
index 0000000000..3fa72f173b
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2002-2024 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.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.NestedExceptionUtils;
+import org.springframework.util.Assert;
+
+/**
+ * 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.
+ *
+ * @author Rossen Stoyanchev
+ * @since 5.3.33
+ */
+public class DisconnectedClientHelper {
+
+ private static final Set EXCEPTION_PHRASES =
+ new HashSet<>(Arrays.asList("broken pipe", "connection reset by peer"));
+
+ private static final Set EXCEPTION_TYPE_NAMES =
+ new HashSet<>(Arrays.asList("AbortedException", "ClientAbortException", "EOFException", "EofException"));
+
+ private final Log logger;
+
+
+ public DisconnectedClientHelper(String logCategory) {
+ Assert.notNull(logCategory, "'logCategory' is required");
+ this.logger = LogFactory.getLog(logCategory);
+ }
+
+
+ /**
+ * Check via {@link #isClientDisconnectedException} if the exception
+ * indicates the remote client disconnected, and if so log a single line
+ * message when DEBUG is on, and a full stacktrace when TRACE is on for
+ * the configured logger.
+ */
+ public boolean checkAndLogClientDisconnectedException(Throwable ex) {
+ if (isClientDisconnectedException(ex)) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Looks like the client has gone away", ex);
+ }
+ else if (logger.isDebugEnabled()) {
+ logger.debug("Looks like the client has gone away: " + ex +
+ " (For a full stack trace, set the log category '" + logger + "' to TRACE level.)");
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Whether the given exception indicates the client has gone away.
+ * Known cases covered:
+ *
+ * - ClientAbortException or EOFException for Tomcat
+ *
- EofException for Jetty
+ *
- IOException "Broken pipe" or "connection reset by peer"
+ *
+ */
+ public static boolean isClientDisconnectedException(Throwable ex) {
+ String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage();
+ if (message != null) {
+ String text = message.toLowerCase();
+ for (String phrase : EXCEPTION_PHRASES) {
+ if (text.contains(phrase)) {
+ return true;
+ }
+ }
+ }
+ return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName());
+ }
+
+}