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:
@@ -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 {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user