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 4ba3441a47..58da642b8c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 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. @@ -39,7 +39,8 @@ public class DisconnectedClientHelper { Set.of("broken pipe", "connection reset by peer"); private static final Set EXCEPTION_TYPE_NAMES = - Set.of("AbortedException", "ClientAbortException", "EOFException", "EofException"); + Set.of("AbortedException", "ClientAbortException", + "EOFException", "EofException", "AsyncRequestNotUsableException"); private final Log logger; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 34174b1b37..5e8167d8b1 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -307,21 +307,34 @@ public class RequestMappingHandlerAdapter exceptions.toArray(arguments); // efficient arraycopy call in ArrayList arguments[arguments.length - 1] = handlerMethod; - return invocable.invoke(exchange, bindingContext, arguments); + return invocable.invoke(exchange, bindingContext, arguments) + .onErrorResume(invocationEx -> + handleExceptionHandlerFailure(exchange, exception, invocationEx, exceptions, invocable)); } catch (Throwable invocationEx) { - if (!disconnectedClientHelper.checkAndLogClientDisconnectedException(invocationEx)) { - // Any other than the original exception (or a cause) is unintended here, - // probably an accident (e.g. failed assertion or the like). - if (!exceptions.contains(invocationEx) && logger.isWarnEnabled()) { - logger.warn(exchange.getLogPrefix() + "Failure in @ExceptionHandler " + invocable, invocationEx); - } - } + return handleExceptionHandlerFailure(exchange, exception, invocationEx, exceptions, invocable); } } return Mono.error(exception); } + private static Mono handleExceptionHandlerFailure( + ServerWebExchange exchange, Throwable exception, Throwable invocationEx, + ArrayList exceptions, InvocableHandlerMethod invocable) { + + if (disconnectedClientHelper.checkAndLogClientDisconnectedException(invocationEx)) { + return Mono.empty(); + } + + // Any other than the original exception (or a cause) is unintended here, + // probably an accident (e.g. failed assertion or the like). + if (!exceptions.contains(invocationEx) && logger.isWarnEnabled()) { + logger.warn(exchange.getLogPrefix() + "Failure in @ExceptionHandler " + invocable, invocationEx); + } + + return Mono.error(exception); + } + @Override public Mono handleError(ServerWebExchange exchange, Throwable ex) { return handleException(exchange, ex, null, null); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java index c936604a3f..b6884d47b9 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java @@ -38,9 +38,11 @@ import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.request.async.AsyncRequestNotUsableException; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; import org.springframework.web.reactive.resource.ResourceWebHandler; @@ -195,6 +197,14 @@ public class DispatcherHandlerErrorTests { assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); } + @Test + void asyncRequestNotUsableFromExceptionHandler() { + ServerWebExchange exchange = MockServerWebExchange.from( + MockServerHttpRequest.post("/request-not-usable-on-exception-handling")); + + StepVerifier.create(this.dispatcherHandler.handle(exchange)).verifyComplete(); + } + @Configuration @SuppressWarnings({"unused", "WeakerAccess"}) @@ -249,6 +259,16 @@ public class DispatcherHandlerErrorTests { public Publisher requestBody(@RequestBody Publisher body) { return Mono.from(body).map(s -> "hello " + s); } + + @RequestMapping("/request-not-usable-on-exception-handling") + public void handle() throws Exception { + throw new IllegalAccessException(); + } + + @ExceptionHandler + public void handleException(IllegalAccessException ex) throws AsyncRequestNotUsableException { + throw new AsyncRequestNotUsableException("Simulated response failure"); + } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index 3e5d1f0dd8..a441082126 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -432,12 +432,13 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments); } catch (Throwable invocationEx) { - if (!disconnectedClientHelper.checkAndLogClientDisconnectedException(invocationEx)) { - // Any other than the original exception (or a cause) is unintended here, - // probably an accident (e.g. failed assertion or the like). - if (!exceptions.contains(invocationEx) && logger.isWarnEnabled()) { - logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx); - } + if (disconnectedClientHelper.checkAndLogClientDisconnectedException(invocationEx)) { + return new ModelAndView(); + } + // Any other than the original exception (or a cause) is unintended here, + // probably an accident (e.g. failed assertion or the like). + if (!exceptions.contains(invocationEx) && logger.isWarnEnabled()) { + logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx); } // Continue with default processing of the original exception... return null; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java index 614f6ba1a1..2373a3cdbd 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolverTests.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.Locale; import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,6 +51,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.async.AsyncRequestNotUsableException; import org.springframework.web.context.support.WebApplicationObjectSupport; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ModelMethodProcessor; @@ -65,6 +67,8 @@ import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.InstanceOfAssertFactories.LIST; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Test fixture with {@link ExceptionHandlerExceptionResolver}. @@ -401,6 +405,20 @@ class ExceptionHandlerExceptionResolverTests { assertExceptionHandledAsBody(mav, "DefaultTestExceptionResolver: IllegalStateException"); } + @Test + void resolveExceptionAsyncRequestNotUsable() throws Exception { + HttpServletResponse response = mock(); + given(response.getOutputStream()).willThrow(new AsyncRequestNotUsableException("Simulated I/O failure")); + + IllegalArgumentException ex = new IllegalArgumentException(); + HandlerMethod handlerMethod = new HandlerMethod(new ResponseBodyController(), "handle"); + this.resolver.afterPropertiesSet(); + ModelAndView mav = this.resolver.resolveException(this.request, response, handlerMethod, ex); + + assertThat(mav).isNotNull(); + assertThat(mav.isEmpty()).isTrue(); + } + private void assertMethodProcessorCount(int resolverCount, int handlerCount) { assertThat(this.resolver.getArgumentResolvers().getResolvers()).hasSize(resolverCount);