From b31550fd856a0ecca2be5f6c8b25fa08de8c7e62 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 29 Feb 2024 17:36:06 +0000 Subject: [PATCH] Align 5.3.x with 6.1.x In preparation for a larger update, start by aligning with 6.1.x, which includes changes for gh-32042 and gh-30232. See gh-32342 --- .../async/StandardServletAsyncWebRequest.java | 17 ++-- .../request/async/WebAsyncManager.java | 24 ++++- .../web/util/DisconnectedClientHelper.java | 97 +++++++++++++++++++ 3 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java 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: + *

+ */ + 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()); + } + +}