WebFlux support for handling of early exceptions

This change enables a WebFlux HandlerAdapter to handle not only the
success scenario when a handler is selected, but also any potential
error signal that may occur instead. This makes it possible to
extend ControllerAdvice support to exceptions from handler mapping
such as a 404, 406, 415, and/or even earlier exceptions from the
WebFilter chain.

Closes gh-22991
This commit is contained in:
rstoyanchev
2022-11-08 15:13:06 +00:00
parent 9d73f81e9c
commit 2878ade980
6 changed files with 179 additions and 51 deletions

View File

@@ -20,6 +20,7 @@ import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -31,12 +32,15 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer;
import org.springframework.web.testfixture.http.server.reactive.bootstrap.ReactorHttpServer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -47,7 +51,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* @author Rossen Stoyanchev
* @author Juergen Hoeller
*/
class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMappingIntegrationTests {
public class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMappingIntegrationTests {
@Override
protected ApplicationContext initApplicationContext() {
@@ -61,38 +65,38 @@ class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMap
@ParameterizedHttpServerTest
void thrownException(HttpServer httpServer) throws Exception {
startServer(httpServer);
doTest("/thrown-exception", "Recovered from error: State");
}
@ParameterizedHttpServerTest
void thrownExceptionWithCause(HttpServer httpServer) throws Exception {
startServer(httpServer);
doTest("/thrown-exception-with-cause", "Recovered from error: State");
}
@ParameterizedHttpServerTest
void thrownExceptionWithCauseToHandle(HttpServer httpServer) throws Exception {
startServer(httpServer);
doTest("/thrown-exception-with-cause-to-handle", "Recovered from error: IO");
}
@ParameterizedHttpServerTest
void errorBeforeFirstItem(HttpServer httpServer) throws Exception {
startServer(httpServer);
doTest("/mono-error", "Recovered from error: Argument");
}
private void doTest(String url, String expected) throws Exception {
assertThat(performGet(url, new HttpHeaders(), String.class).getBody()).isEqualTo(expected);
}
@ParameterizedHttpServerTest // SPR-16051
void exceptionAfterSeveralItems(HttpServer httpServer) throws Exception {
startServer(httpServer);
assertThatExceptionOfType(Throwable.class).isThrownBy(() ->
performGet("/SPR-16051", new HttpHeaders(), String.class).getBody())
.withMessageStartingWith("Error while extracting response");
assertThatExceptionOfType(Throwable.class)
.isThrownBy(() -> performGet("/SPR-16051", new HttpHeaders(), String.class))
.withMessageStartingWith("Error while extracting response");
}
@ParameterizedHttpServerTest // SPR-16318
@@ -101,19 +105,49 @@ class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMap
HttpHeaders headers = new HttpHeaders();
headers.add("Accept", "text/plain, application/problem+json");
assertThatExceptionOfType(HttpStatusCodeException.class).isThrownBy(() ->
performGet("/SPR-16318", headers, String.class).getBody())
.satisfies(ex -> {
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(ex.getResponseHeaders().getContentType().toString()).isEqualTo("application/problem+json");
assertThat(ex.getResponseBodyAsString()).isEqualTo("{\"reason\":\"error\"}");
});
assertThatExceptionOfType(HttpStatusCodeException.class)
.isThrownBy(() -> performGet("/SPR-16318", headers, String.class))
.satisfies(ex -> {
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(ex.getResponseHeaders().getContentType().toString()).isEqualTo("application/problem+json");
assertThat(ex.getResponseBodyAsString()).isEqualTo("{\"reason\":\"error\"}");
});
}
private void doTest(String url, String expected) throws Exception {
assertThat(performGet(url, new HttpHeaders(), String.class).getBody()).isEqualTo(expected);
@Test
public void globalExceptionHandlerWithHandlerNotFound() throws Exception {
startServer(new ReactorHttpServer());
assertThatExceptionOfType(HttpStatusCodeException.class)
.isThrownBy(() -> performGet("/no-such-handler", new HttpHeaders(), String.class))
.satisfies(ex -> {
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(ex.getResponseBodyAsString()).isEqualTo("" +
"{\"type\":\"about:blank\"," +
"\"title\":\"Not Found\"," +
"\"status\":404," +
"\"instance\":\"/no-such-handler\"}");
});
}
@Test
public void globalExceptionHandlerWithMissingRequestParameter() throws Exception {
startServer(new ReactorHttpServer());
assertThatExceptionOfType(HttpStatusCodeException.class)
.isThrownBy(() -> performGet("/missing-request-parameter", new HttpHeaders(), String.class))
.satisfies(ex -> {
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(ex.getResponseBodyAsString()).isEqualTo("{" +
"\"type\":\"about:blank\"," +
"\"title\":\"Bad Request\"," +
"\"status\":400," +
"\"detail\":\"Required query parameter 'q' is not present.\"," +
"\"instance\":\"/missing-request-parameter\"}");
});
}
@Configuration
@EnableWebFlux
@@ -147,6 +181,11 @@ class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMap
return Mono.error(new IllegalArgumentException("Argument"));
}
@GetMapping(path = "/missing-request-parameter")
public String handleWithMissingParameter(@RequestParam String q) {
return "Success, q:" + q;
}
@GetMapping("/SPR-16051")
public Flux<String> errors() {
return Flux.range(1, 10000)
@@ -185,6 +224,11 @@ class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMap
}
@ControllerAdvice
private static class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
}
@SuppressWarnings("serial")
private static class Spr16318Exception extends Exception {
}