Add ErrorResponse and ErrorResponseException

ErrorResponse represents a complete error response with status, headers,
and an  RFC 7807 ProblemDetail body.

ErrorResponseException implements ErrorResponse and is usable on its
own or as a base class. ResponseStatusException extends
ErrorResponseException and now also supports RFC 7807 and so does its
sub-hierarchy.

ErrorResponse can be returned from `@ExceptionHandler` methods and is
mapped to ResponseEntity.

See gh-27052
This commit is contained in:
rstoyanchev
2022-02-23 14:52:17 +00:00
parent 714d451260
commit 3efedef161
13 changed files with 415 additions and 90 deletions

View File

@@ -37,6 +37,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.ErrorResponse;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
@@ -44,7 +45,7 @@ import org.springframework.web.server.ServerWebExchange;
/**
* Handles return values of type {@link HttpEntity}, {@link ResponseEntity},
* {@link HttpHeaders}, and {@link ProblemDetail}.
* {@link HttpHeaders}, {@link ErrorResponse}, and {@link ProblemDetail}.
*
* <p>By default the order for this result handler is set to 0. It is generally
* safe to place it early in the order as it looks for a concrete return type.
@@ -108,7 +109,8 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
return false;
}
return ((HttpEntity.class.isAssignableFrom(type) && !RequestEntity.class.isAssignableFrom(type)) ||
HttpHeaders.class.isAssignableFrom(type) || ProblemDetail.class.isAssignableFrom(type));
ErrorResponse.class.isAssignableFrom(type) || ProblemDetail.class.isAssignableFrom(type) ||
HttpHeaders.class.isAssignableFrom(type));
}
@@ -138,12 +140,15 @@ public class ResponseEntityResultHandler extends AbstractMessageWriterResultHand
if (returnValue instanceof HttpEntity) {
httpEntity = (HttpEntity<?>) returnValue;
}
else if (returnValue instanceof HttpHeaders) {
httpEntity = new ResponseEntity<>((HttpHeaders) returnValue, HttpStatus.OK);
else if (returnValue instanceof ErrorResponse response) {
httpEntity = new ResponseEntity<>(response.getBody(), response.getHeaders(), response.getRawStatusCode());
}
else if (returnValue instanceof ProblemDetail detail) {
httpEntity = new ResponseEntity<>(returnValue, HttpHeaders.EMPTY, detail.getStatus());
}
else if (returnValue instanceof HttpHeaders) {
httpEntity = new ResponseEntity<>((HttpHeaders) returnValue, HttpStatus.OK);
}
else {
throw new IllegalArgumentException(
"HttpEntity or HttpHeaders expected but got: " + returnValue.getClass());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -330,7 +330,7 @@ public class RequestMappingInfoHandlerMappingTests {
UnsupportedMediaTypeStatusException umtse = (UnsupportedMediaTypeStatusException) ex;
MediaType mediaType = new MediaType("foo", "bar");
assertThat(umtse.getSupportedMediaTypes()).containsExactly(mediaType);
assertThat(umtse.getResponseHeaders().getAcceptPatch()).containsExactly(mediaType);
assertThat(umtse.getHeaders().getAcceptPatch()).containsExactly(mediaType);
})
.verify();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -55,6 +55,8 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.ObjectUtils;
import org.springframework.web.ErrorResponse;
import org.springframework.web.ErrorResponseException;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
@@ -130,6 +132,9 @@ public class ResponseEntityResultHandlerTests {
returnType = on(TestController.class).resolveReturnType(HttpHeaders.class);
assertThat(this.resultHandler.supports(handlerResult(value, returnType))).isTrue();
returnType = on(TestController.class).resolveReturnType(ErrorResponse.class);
assertThat(this.resultHandler.supports(handlerResult(value, returnType))).isTrue();
returnType = on(TestController.class).resolveReturnType(ProblemDetail.class);
assertThat(this.resultHandler.supports(handlerResult(value, returnType))).isTrue();
@@ -236,6 +241,28 @@ public class ResponseEntityResultHandlerTests {
testHandle(returnValue, returnType);
}
@Test
public void handleErrorResponse() {
ErrorResponseException ex = new ErrorResponseException(HttpStatus.BAD_REQUEST);
ex.getHeaders().add("foo", "bar");
MethodParameter returnType = on(TestController.class).resolveReturnType(ErrorResponse.class);
HandlerResult result = handlerResult(ex, returnType);
MockServerWebExchange exchange = MockServerWebExchange.from(get("/path"));
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_PROBLEM_JSON);
this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5));
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(exchange.getResponse().getHeaders()).hasSize(3);
assertThat(exchange.getResponse().getHeaders().get("foo")).containsExactly("bar");
assertThat(exchange.getResponse().getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PROBLEM_JSON);
assertResponseBody(exchange,
"{\"type\":\"about:blank\"," +
"\"title\":\"Bad Request\"," +
"\"status\":400," +
"\"detail\":null," +
"\"instance\":\"/path\"}");
}
@Test
public void handleProblemDetail() {
ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
@@ -529,6 +556,8 @@ public class ResponseEntityResultHandlerTests {
ResponseEntity<Person> responseEntityPerson() { return null; }
ErrorResponse errorResponse() { return null; }
ProblemDetail problemDetail() { return null; }
HttpHeaders httpHeaders() { return null; }