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 91664d306a2..16875352fda 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 jakarta.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; @@ -35,6 +34,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 @@ -68,6 +68,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(); @@ -350,10 +360,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; } } @@ -394,9 +403,12 @@ public final class WebAsyncManager { return; } + if (result instanceof Exception ex && disconnectedClientHelper.checkAndLogClientDisconnectedException(ex)) { + 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 00000000000..4ba3441a472 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2023 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.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 6.1 + */ +public class DisconnectedClientHelper { + + private static final Set EXCEPTION_PHRASES = + Set.of("broken pipe", "connection reset by peer"); + + private static final Set EXCEPTION_TYPE_NAMES = + Set.of("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: + * + */ + 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()); + } + +}