Improve mapping any Exception to ErrorResponse

Add protected, convenience method in ResponseEntityExceptionHandler
to create a ProblemDetail for any exception, along with a
MessageSource lookup for the "detail" field.

Closes gh-29384
This commit is contained in:
rstoyanchev
2022-11-01 11:40:23 +00:00
parent 210019cad1
commit 506fbe5243
5 changed files with 171 additions and 52 deletions

View File

@@ -292,6 +292,35 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
return handleExceptionInternal(ex, null, headers, status, exchange);
}
/**
* Convenience method to create a {@link ProblemDetail} for any exception
* that doesn't implement {@link ErrorResponse}, also performing a
* {@link MessageSource} lookup for the "detail" field.
* @param ex the exception being handled
* @param status the status to associate with the exception
* @param defaultDetail default value for the "detail" field
* @param detailMessageCode the code to use to look up the "detail" field
* through a {@code MessageSource}, falling back on
* {@link ErrorResponse#getDefaultDetailMessageCode(Class, String)}
* @param detailMessageArguments the arguments to go with the detailMessageCode
* @return the created {@code ProblemDetail} instance
*/
protected ProblemDetail createProblemDetail(
Exception ex, HttpStatusCode status, @Nullable HttpHeaders headers,
String defaultDetail, @Nullable String detailMessageCode, @Nullable Object[] detailMessageArguments,
ServerWebExchange exchange) {
ErrorResponse response = ErrorResponse.createFor(
ex, status, headers, defaultDetail, detailMessageCode, detailMessageArguments);
return response.updateAndGetBody(this.messageSource, getLocale(exchange));
}
private static Locale getLocale(ServerWebExchange exchange) {
Locale locale = exchange.getLocaleContext().getLocale();
return (locale != null ? locale : Locale.getDefault());
}
/**
* Internal handler method that all others in this class delegate to, for
* common handling, and for the creation of a {@link ResponseEntity}.
@@ -311,7 +340,7 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
* @return a {@code Mono} with the {@code ResponseEntity} for the response
*/
protected Mono<ResponseEntity<Object>> handleExceptionInternal(
Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode status,
Exception ex, @Nullable Object body, @Nullable HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
if (exchange.getResponse().isCommitted()) {
@@ -319,25 +348,12 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
}
if (body == null && ex instanceof ErrorResponse errorResponse) {
body = resolveDetailViaMessageSource(errorResponse, exchange.getLocaleContext().getLocale());
body = errorResponse.updateAndGetBody(this.messageSource, getLocale(exchange));
}
return createResponseEntity(body, headers, status, exchange);
}
private ProblemDetail resolveDetailViaMessageSource(ErrorResponse response, @Nullable Locale locale) {
ProblemDetail body = response.getBody();
if (this.messageSource != null) {
locale = (locale != null ? locale : Locale.getDefault());
Object[] arguments = response.getDetailMessageArguments(this.messageSource, locale);
String detail = this.messageSource.getMessage(response.getDetailMessageCode(), arguments, null, locale);
if (detail != null) {
body.setDetail(detail);
}
}
return body;
}
/**
* Create the {@link ResponseEntity} to use from the given body, headers,
* and statusCode. Subclasses can override this method to inspect and possibly
@@ -351,7 +367,8 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
* @since 6.0
*/
protected Mono<ResponseEntity<Object>> createResponseEntity(
@Nullable Object body, HttpHeaders headers, HttpStatusCode status, ServerWebExchange exchange) {
@Nullable Object body, @Nullable HttpHeaders headers, HttpStatusCode status,
ServerWebExchange exchange) {
return Mono.just(new ResponseEntity<>(body, headers, status));
}

View File

@@ -59,7 +59,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class ResponseEntityExceptionHandlerTests {
private final ResponseEntityExceptionHandler exceptionHandler = new GlobalExceptionHandler();
private final GlobalExceptionHandler exceptionHandler = new GlobalExceptionHandler();
private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
@@ -149,6 +149,29 @@ public class ResponseEntityExceptionHandlerTests {
"Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]");
}
@Test
void customExceptionToProblemDetailViaMessageSource() {
Locale locale = Locale.UK;
LocaleContextHolder.setLocale(locale);
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage(
"problemDetail." + IllegalStateException.class.getName(), locale,
"Invalid state: {0}");
this.exceptionHandler.setMessageSource(messageSource);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")
.acceptLanguageAsLocales(locale).build());
ResponseEntity<?> responseEntity =
this.exceptionHandler.handleException(new IllegalStateException(), exchange).block();
ProblemDetail body = (ProblemDetail) responseEntity.getBody();
assertThat(body.getDetail()).isEqualTo("Invalid state: A");
}
@SuppressWarnings("unchecked")
private ResponseEntity<ProblemDetail> testException(ErrorResponseException exception) {
@@ -247,6 +270,12 @@ public class ResponseEntityExceptionHandlerTests {
return handleAndSetTypeToExceptionName(ex, headers, status, exchange);
}
public Mono<ResponseEntity<Object>> handleException(IllegalStateException ex, ServerWebExchange exchange) {
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
ProblemDetail body = createProblemDetail(ex, status, null, ex.getMessage(), null, new Object[] {"A"}, exchange);
return handleExceptionInternal(ex, body, null, status, exchange);
}
}
}