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 0000000000..45198fe728
--- /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 2c08cc78bb..2bb7158b58 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
@@ -19,14 +19,17 @@ package org.springframework.web.context.request.async;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -45,8 +48,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 timeoutHandlers = new ArrayList<>();
private final List> exceptionHandlers = new ArrayList<>();
@@ -59,6 +60,10 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
@Nullable
private AsyncContext asyncContext;
+ private final AtomicReference state;
+
+ private volatile boolean hasError;
+
/**
* Create a new instance for the given request/response pair.
@@ -66,7 +71,32 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
* @param response current HTTP response
*/
public StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
- super(request, response);
+ this(request, response, null);
+ }
+
+ /**
+ * Constructor to wrap the request and response for the current dispatch that
+ * also picks up the state of the last (probably the REQUEST) dispatch.
+ * @param request current HTTP request
+ * @param response current HTTP response
+ * @param previousRequest the existing request from the last dispatch
+ * @since 5.3.33
+ */
+ StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response,
+ @Nullable StandardServletAsyncWebRequest previousRequest) {
+
+ super(request, new LifecycleHttpServletResponse(response));
+
+ if (previousRequest != null) {
+ this.state = previousRequest.state;
+ this.hasError = previousRequest.hasError;
+ }
+ else {
+ this.state = new AtomicReference<>(State.ACTIVE);
+ }
+
+ //noinspection DataFlowIssue
+ ((LifecycleHttpServletResponse) getResponse()).setParent(this);
}
@@ -107,7 +137,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
*/
@Override
public boolean isAsyncComplete() {
- return this.asyncCompleted.get();
+ return (this.state.get() == State.COMPLETED);
}
@Override
@@ -117,6 +147,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
"in async request processing. This is done in Java code using the Servlet API " +
"or by adding \"true\" to servlet and " +
"filter declarations in web.xml.");
+
Assert.state(!isAsyncComplete(), "Async processing has already completed");
if (isAsyncStarted()) {
@@ -131,8 +162,10 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
@Override
public void dispatch() {
- Assert.state(this.asyncContext != null, "Cannot dispatch without an AsyncContext");
- this.asyncContext.dispatch();
+ Assert.state(this.asyncContext != null, "AsyncContext not yet initialized");
+ if (!this.isAsyncComplete()) {
+ this.asyncContext.dispatch();
+ }
}
@@ -151,14 +184,152 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
@Override
public void onError(AsyncEvent event) throws IOException {
+ transitionToErrorState();
this.exceptionHandlers.forEach(consumer -> consumer.accept(event.getThrowable()));
}
+ private void transitionToErrorState() {
+ this.hasError = true;
+ this.state.compareAndSet(State.ACTIVE, State.ERROR);
+ }
+
@Override
public void onComplete(AsyncEvent event) throws IOException {
this.completionHandlers.forEach(Runnable::run);
this.asyncContext = null;
- this.asyncCompleted.set(true);
+ this.state.set(State.COMPLETED);
+ }
+
+
+ /**
+ * Response wrapper to wrap the output stream with {@link LifecycleServletOutputStream}.
+ */
+ private static final class LifecycleHttpServletResponse extends HttpServletResponseWrapper {
+
+ @Nullable
+ private StandardServletAsyncWebRequest parent;
+
+ private ServletOutputStream outputStream;
+
+ public LifecycleHttpServletResponse(HttpServletResponse response) {
+ super(response);
+ }
+
+ public void setParent(StandardServletAsyncWebRequest parent) {
+ this.parent = parent;
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() {
+ if (this.outputStream == null) {
+ Assert.notNull(this.parent, "Not initialized");
+ this.outputStream = new LifecycleServletOutputStream((HttpServletResponse) getResponse(), this.parent);
+ }
+ return this.outputStream;
+ }
+ }
+
+
+ /**
+ * Wraps a ServletOutputStream to prevent use after Servlet container onError
+ * notifications, and after async request completion.
+ */
+ private static final class LifecycleServletOutputStream extends ServletOutputStream {
+
+ private final HttpServletResponse response;
+
+ private final StandardServletAsyncWebRequest parent;
+
+ private LifecycleServletOutputStream(HttpServletResponse response, StandardServletAsyncWebRequest parent) {
+ this.response = response;
+ this.parent = parent;
+ }
+
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ checkState();
+ try {
+ this.response.getOutputStream().write(b);
+ }
+ catch (IOException ex) {
+ handleIOException(ex, "ServletOutputStream failed to write");
+ }
+ }
+
+ public void write(byte[] buf, int offset, int len) throws IOException {
+ checkState();
+ try {
+ this.response.getOutputStream().write(buf, offset, len);
+ }
+ catch (IOException ex) {
+ handleIOException(ex, "ServletOutputStream failed to write");
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ checkState();
+ try {
+ this.response.getOutputStream().flush();
+ }
+ catch (IOException ex) {
+ handleIOException(ex, "ServletOutputStream failed to flush");
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ checkState();
+ try {
+ this.response.getOutputStream().close();
+ }
+ catch (IOException ex) {
+ handleIOException(ex, "ServletOutputStream failed to close");
+ }
+ }
+
+ private void checkState() throws AsyncRequestNotUsableException {
+ if (this.parent.state.get() != State.ACTIVE) {
+ String reason = this.parent.state.get() == State.COMPLETED ?
+ "async request completion" : "Servlet container onError notification";
+ throw new AsyncRequestNotUsableException("Response not usable after " + reason + ".");
+ }
+ }
+
+ private void handleIOException(IOException ex, String msg) throws AsyncRequestNotUsableException {
+ this.parent.transitionToErrorState();
+ throw new AsyncRequestNotUsableException(msg, ex);
+ }
+
+ }
+
+
+ /**
+ * Represents a state for {@link StandardServletAsyncWebRequest} to be in.
+ *
+ * ACTIVE ----+
+ * | |
+ * v |
+ * ERROR |
+ * | |
+ * v |
+ * COMPLETED <--+
+ *
+ * @since 5.3.33
+ */
+ private enum State {
+
+ ACTIVE, ERROR, 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 63a3268b66..159e06da33 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 abd2336ab1..de3e49c726 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-2018 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 ?
+ new StandardServletAsyncWebRequest(request, response, (StandardServletAsyncWebRequest) prev) :
+ new StandardServletAsyncWebRequest(request, response));
}
}
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
index 3fa72f173b..f3d2197b11 100644
--- a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java
+++ b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java
@@ -41,7 +41,8 @@ public class DisconnectedClientHelper {
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"));
+ new HashSet<>(Arrays.asList("AbortedException", "ClientAbortException",
+ "EOFException", "EofException", "AsyncRequestNotUsableException"));
private final Log logger;
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 c68bd3ef29..fe780a3e69 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
@@ -853,7 +853,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));
+
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
@@ -873,15 +887,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();