Reuse existing async timeout in DefaultAsyncServerResponse

Prior to this commit, calling `DefaultAsyncServerResponse.writeAsync()`
would  unconditionally create a new `AsyncWebRequest` and install it
on the `WebAsyncManager`, even when one is already present for the
current request.
The functional web framework can do such a thing when returning a
`ServerResponse.async(future)` from a `HandlerFunction`; the
`HandlerFunctionAdapter` does install an async web request already.

This means that the async timeout configured at the application level
would be ignored and instead falling back to the Servlet container
default.

This commit makes the `DefaultAsyncServerResponse` skip async web
request creation it there is an existing one.

Fixes gh-37257
This commit is contained in:
Brian Clozel
2026-09-08 11:48:40 +02:00
parent 4acd6d6a7e
commit 3c6b001349
2 changed files with 33 additions and 2 deletions
@@ -115,8 +115,11 @@ final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse imple
throws ServletException, IOException {
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncManager.setAsyncWebRequest(asyncWebRequest);
AsyncWebRequest asyncWebRequest = asyncManager.getAsyncWebRequest();
if (asyncWebRequest == null) {
asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncManager.setAsyncWebRequest(asyncWebRequest);
}
try {
asyncManager.startDeferredResultProcessing(deferredResult);
}
@@ -18,15 +18,43 @@ package org.springframework.web.servlet.function;
import java.util.concurrent.CompletableFuture;
import jakarta.servlet.AsyncContext;
import org.junit.jupiter.api.Test;
import org.springframework.web.context.request.async.AsyncWebRequest;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultAsyncServerResponse}.
* @author Arjen Poutsma
*/
class DefaultAsyncServerResponseTests {
@Test
void writeAsyncReusesExistingAsyncWebRequestTimeout() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
request.setAsyncSupported(true);
MockHttpServletResponse response = new MockHttpServletResponse();
AsyncWebRequest existingAsyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
existingAsyncWebRequest.setTimeout(1000L);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setAsyncWebRequest(existingAsyncWebRequest);
DeferredResult<Object> deferredResult = new DeferredResult<>();
DefaultAsyncServerResponse.writeAsync(request, response, deferredResult);
assertThat(asyncManager.getAsyncWebRequest()).isSameAs(existingAsyncWebRequest);
AsyncContext asyncContext = request.getAsyncContext();
assertThat(asyncContext.getTimeout()).isEqualTo(1000L);
}
@Test
void blockCompleted() {
ServerResponse wrappee = ServerResponse.ok().build();