Add state and response wrapping to StandardServletAsyncWebRequest
The wrapped response prevents use after AsyncListener onError or completion to ensure compliance with Servlet Spec 2.3.3.4. The wrapped response is applied in RequestMappingHandlerAdapter. The wrapped response raises AsyncRequestNotUsableException that is now handled in DefaultHandlerExceptionResolver. See gh-32340
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
* <td><div class="block">400 (SC_BAD_REQUEST)</div></td>
|
||||
* </tr>
|
||||
* <tr class="odd-row-color">
|
||||
* <td><div class="block">{@link MethodValidationException}</div></td>
|
||||
* <td><div class="block">500 (SC_INTERNAL_SERVER_ERROR)</div></td>
|
||||
* </tr>
|
||||
* <tr class="odd-row-color">
|
||||
* <td><div class="block">{@link HandlerMethodValidationException}</div></td>
|
||||
* <td><div class="block">400 (SC_BAD_REQUEST)</div></td>
|
||||
* </tr>
|
||||
@@ -136,9 +141,9 @@ import org.springframework.web.util.WebUtils;
|
||||
* <td><div class="block">AsyncRequestTimeoutException</div></td>
|
||||
* <td><div class="block">503 (SC_SERVICE_UNAVAILABLE)</div></td>
|
||||
* </tr>
|
||||
* <tr class="odd-row-color">
|
||||
* <td><div class="block">{@link MethodValidationException}</div></td>
|
||||
* <td><div class="block">500 (SC_INTERNAL_SERVER_ERROR)</div></td>
|
||||
* <tr class="even-row-color">
|
||||
* <td><div class="block">AsyncRequestNotUsableException</div></td>
|
||||
* <td><div class="block">Not applicable</div></td>
|
||||
* </tr>
|
||||
* </tbody>
|
||||
* </table>
|
||||
@@ -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.
|
||||
* <p>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.
|
||||
* <p>The default implementation sets status and the headers of the response
|
||||
|
||||
@@ -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<Exception>) exceptionType));
|
||||
|
||||
Reference in New Issue
Block a user