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:
rstoyanchev
2024-03-01 22:31:09 +00:00
parent 380184e85a
commit 379ffac508
8 changed files with 622 additions and 31 deletions

View File

@@ -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();

View File

@@ -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

View File

@@ -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));