diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestNotUsableException.java b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestNotUsableException.java new file mode 100644 index 00000000000..45198fe728d --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestNotUsableException.java @@ -0,0 +1,44 @@ +/* + * 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.context.request.async; + +import java.io.IOException; + +/** + * Raised when the response for an asynchronous request becomes unusable as + * indicated by a write failure, or a Servlet container error notification, or + * after the async request has completed. + * + *
The exception relies on response wrapping, and on {@code AsyncListener}
+ * notifications, managed by {@link StandardServletAsyncWebRequest}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 5.3.33
+ */
+@SuppressWarnings("serial")
+public class AsyncRequestNotUsableException extends IOException {
+
+
+ public AsyncRequestNotUsableException(String message) {
+ super(message);
+ }
+
+ public AsyncRequestNotUsableException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
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 eb46ccb6479..aa48427b2ad 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.
@@ -17,16 +17,22 @@
package org.springframework.web.context.request.async;
import java.io.IOException;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.Locale;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.AsyncListener;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpServletResponseWrapper;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -45,8 +51,6 @@ import org.springframework.web.context.request.ServletWebRequest;
*/
public class StandardServletAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest, AsyncListener {
- private final AtomicBoolean asyncCompleted = new AtomicBoolean();
-
private final List By default, do nothing since the response is not usable.
+ * @param ex the {@link AsyncRequestTimeoutException} to be handled
+ * @param request current HTTP request
+ * @param response current HTTP response
+ * @param handler the executed handler, or {@code null} if none chosen
+ * at the time of the exception (for example, if multipart resolution failed)
+ * @return an empty ModelAndView indicating the exception was handled
+ * @throws IOException potentially thrown from {@link HttpServletResponse#sendError}
+ * @since 5.3.33
+ */
+ protected ModelAndView handleAsyncRequestNotUsableException(AsyncRequestNotUsableException ex,
+ HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) {
+
+ return new ModelAndView();
+ }
+
/**
* Handle an {@link ErrorResponse} exception.
* The default implementation sets status and the headers of the response
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java
index fa743061910..1f43d95a862 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java
@@ -108,6 +108,10 @@ class ResponseEntityExceptionHandlerTests {
.filter(method -> method.getName().startsWith("handle") && (method.getParameterCount() == 4))
.filter(method -> !method.getName().equals("handleErrorResponse"))
.map(method -> method.getParameterTypes()[0])
+ .filter(exceptionType -> {
+ String name = exceptionType.getSimpleName();
+ return !name.equals("AsyncRequestNotUsableException");
+ })
.forEach(exceptionType -> assertThat(annotation.value())
.as("@ExceptionHandler is missing declaration for " + exceptionType.getName())
.contains((Class
+ * NEW
+ * |
+ * v
+ * ASYNC----> +
+ * | |
+ * v |
+ * ERROR |
+ * | |
+ * v |
+ * COMPLETED <--+
+ *
+ * @since 5.3.33
+ */
+ private enum State {
+
+ /** New request (thas may not do async handling). */
+ NEW,
+
+ /** Async handling has started. */
+ ASYNC,
+
+ /** onError notification received, or ServletOutputStream failed. */
+ ERROR,
+
+ /** onComplete notification received. */
+ COMPLETED
+
}
}
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 35e901b59ea..0d52d29a30c 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
@@ -132,6 +132,15 @@ public final class WebAsyncManager {
WebAsyncUtils.WEB_ASYNC_MANAGER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST));
}
+ /**
+ * Return the current {@link AsyncWebRequest}.
+ * @since 5.3.33
+ */
+ @Nullable
+ public AsyncWebRequest getAsyncWebRequest() {
+ return this.asyncWebRequest;
+ }
+
/**
* Configure an AsyncTaskExecutor for use with concurrent processing via
* {@link #startCallableProcessing(Callable, Object...)}.
diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java
index 7c948f1037f..4d089061f6c 100644
--- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java
+++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.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.
@@ -82,7 +82,10 @@ public abstract class WebAsyncUtils {
* @return an AsyncWebRequest instance (never {@code null})
*/
public static AsyncWebRequest createAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
- return new StandardServletAsyncWebRequest(request, response);
+ AsyncWebRequest prev = getAsyncManager(request).getAsyncWebRequest();
+ return (prev instanceof StandardServletAsyncWebRequest standardRequest ?
+ new StandardServletAsyncWebRequest(request, response, standardRequest) :
+ new StandardServletAsyncWebRequest(request, response));
}
}
diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java
index b5bee10a06b..280024d944e 100644
--- a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java
+++ b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java
@@ -94,9 +94,8 @@ class StandardServletAsyncWebRequestTests {
@Test
void startAsyncAfterCompleted() throws Exception {
this.asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(this.request, this.response)));
- assertThatIllegalStateException().isThrownBy(
- this.asyncRequest::startAsync)
- .withMessage("Async processing has already completed");
+ assertThatIllegalStateException().isThrownBy(this.asyncRequest::startAsync)
+ .withMessage("Cannot start async: [COMPLETED]");
}
@Test
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java
index 4ffe4916500..17776f3fe97 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java
@@ -875,7 +875,21 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
- ServletWebRequest webRequest = new ServletWebRequest(request, response);
+ WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+ AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
+ asyncWebRequest.setTimeout(this.asyncRequestTimeout);
+
+ asyncManager.setTaskExecutor(this.taskExecutor);
+ asyncManager.setAsyncWebRequest(asyncWebRequest);
+ asyncManager.registerCallableInterceptors(this.callableInterceptors);
+ asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
+
+ // Obtain wrapped response to enforce lifecycle rule from Servlet spec, section 2.3.3.4
+ response = asyncWebRequest.getNativeResponse(HttpServletResponse.class);
+
+ ServletWebRequest webRequest = (asyncWebRequest instanceof ServletWebRequest ?
+ (ServletWebRequest) asyncWebRequest : new ServletWebRequest(request, response));
+
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
@@ -895,15 +909,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
- AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
- asyncWebRequest.setTimeout(this.asyncRequestTimeout);
-
- WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
- asyncManager.setTaskExecutor(this.taskExecutor);
- asyncManager.setAsyncWebRequest(asyncWebRequest);
- asyncManager.registerCallableInterceptors(this.callableInterceptors);
- asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
-
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
Object[] resultContext = asyncManager.getConcurrentResultContext();
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java
index 2e9afabe095..1e798f94c34 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java
@@ -45,6 +45,7 @@ import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.multipart.MultipartFile;
@@ -121,6 +122,10 @@ import org.springframework.web.util.WebUtils;
*
*
*
+ *
+ *
+ *
+ *
*
@@ -136,9 +141,9 @@ import org.springframework.web.util.WebUtils;
*
*
*
*
*
- *
- *
- *
+ *
+ *
*
*
@@ -243,6 +248,10 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
else if (ex instanceof BindException theEx) {
return handleBindException(theEx, request, response, handler);
}
+ else if (ex instanceof AsyncRequestNotUsableException) {
+ return handleAsyncRequestNotUsableException(
+ (AsyncRequestNotUsableException) ex, request, response, handler);
+ }
}
catch (Exception handlerEx) {
if (logger.isWarnEnabled()) {
@@ -494,6 +503,24 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
return null;
}
+ /**
+ * Handle the case of an I/O failure from the ServletOutputStream.
+ *
+ *
*