Skip to content

Commit 7a0fe7d

Browse files
committed
WebAsyncManager wraps disconnected client errors
If the Servlet container delegates a disconnected client error via AsyncListener#onError, wrap it as AsyncRequestNotUsableException for more targeted and consistent handling of such errors. Closes gh-34363
1 parent ccdaed5 commit 7a0fe7d

File tree

2 files changed

+43
-2
lines changed

2 files changed

+43
-2
lines changed

spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@
3434
import org.springframework.util.Assert;
3535
import org.springframework.web.context.request.RequestAttributes;
3636
import org.springframework.web.context.request.async.DeferredResult.DeferredResultHandler;
37+
import org.springframework.web.util.DisconnectedClientHelper;
3738

3839
/**
3940
* The central class for managing asynchronous request processing, mainly intended
@@ -350,6 +351,10 @@ public void startCallableProcessing(final WebAsyncTask<?> webAsyncTask, Object..
350351
if (logger.isDebugEnabled()) {
351352
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest) + ": " + ex);
352353
}
354+
if (DisconnectedClientHelper.isClientDisconnectedException(ex)) {
355+
ex = new AsyncRequestNotUsableException(
356+
"Servlet container error notification for disconnected client", ex);
357+
}
353358
Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex);
354359
result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex);
355360
setConcurrentResultAndDispatch(result);
@@ -442,6 +447,10 @@ public void startDeferredResultProcessing(
442447
if (logger.isDebugEnabled()) {
443448
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest));
444449
}
450+
if (DisconnectedClientHelper.isClientDisconnectedException(ex)) {
451+
ex = new AsyncRequestNotUsableException(
452+
"Servlet container error notification for disconnected client", ex);
453+
}
445454
try {
446455
interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex);
447456
synchronized (WebAsyncManager.this) {

spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerErrorTests.java

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.context.request.async;
1818

19+
import java.io.IOException;
1920
import java.util.concurrent.Callable;
2021

2122
import jakarta.servlet.AsyncEvent;
@@ -152,6 +153,22 @@ void startCallableProcessingAfterException() throws Exception {
152153
verify(interceptor).beforeConcurrentHandling(this.asyncWebRequest, callable);
153154
}
154155

156+
@Test // gh-34363
157+
void startCallableProcessingDisconnectedClient() throws Exception {
158+
StubCallable callable = new StubCallable();
159+
this.asyncManager.startCallableProcessing(callable);
160+
161+
IOException ex = new IOException("broken pipe");
162+
AsyncEvent event = new AsyncEvent(new MockAsyncContext(this.servletRequest, this.servletResponse), ex);
163+
this.asyncWebRequest.onError(event);
164+
165+
MockAsyncContext asyncContext = (MockAsyncContext) this.servletRequest.getAsyncContext();
166+
assertThat(this.asyncManager.hasConcurrentResult()).isTrue();
167+
assertThat(this.asyncManager.getConcurrentResult())
168+
.as("Disconnected client error not wrapped AsyncRequestNotUsableException")
169+
.isOfAnyClassIn(AsyncRequestNotUsableException.class);
170+
}
171+
155172
@Test
156173
void startDeferredResultProcessingErrorAndComplete() throws Exception {
157174

@@ -259,6 +276,21 @@ public <T> boolean handleError(NativeWebRequest request, DeferredResult<T> resul
259276
assertThat(((MockAsyncContext) this.servletRequest.getAsyncContext()).getDispatchedPath()).isEqualTo("/test");
260277
}
261278

279+
@Test // gh-34363
280+
void startDeferredResultProcessingDisconnectedClient() throws Exception {
281+
DeferredResult<Object> deferredResult = new DeferredResult<>();
282+
this.asyncManager.startDeferredResultProcessing(deferredResult);
283+
284+
IOException ex = new IOException("broken pipe");
285+
AsyncEvent event = new AsyncEvent(new MockAsyncContext(this.servletRequest, this.servletResponse), ex);
286+
this.asyncWebRequest.onError(event);
287+
288+
assertThat(this.asyncManager.hasConcurrentResult()).isTrue();
289+
assertThat(deferredResult.getResult())
290+
.as("Disconnected client error not wrapped AsyncRequestNotUsableException")
291+
.isOfAnyClassIn(AsyncRequestNotUsableException.class);
292+
}
293+
262294

263295
private static final class StubCallable implements Callable<Object> {
264296
@Override

0 commit comments

Comments
 (0)