Browse Source

Add ProblemDetail "type" message code

See gh-30566
pull/30690/head
rstoyanchev 3 years ago
parent
commit
9c7b5cb3f5
  1. 28
      framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc
  2. 27
      framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc
  3. 35
      spring-web/src/main/java/org/springframework/web/ErrorResponse.java
  4. 17
      spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandlerTests.java
  5. 15
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java

28
framework-docs/modules/ROOT/pages/web/webflux/ann-rest-exceptions.adoc

@ -71,20 +71,21 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an @@ -71,20 +71,21 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an
[.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-i18n[See equivalent in the Servlet stack]#
It is a common requirement to internationalize error response details, and good practice
to customize the problem details for Spring WebFlux exceptions. This is supported as follows:
to customize the problem details for Spring WebFlux exceptions. This section describes the
support for that.
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource].
The actual message code value is parameterized with placeholders, e.g.
`+"HTTP method {0} not supported"+` to be expanded from the arguments.
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
"detail" and the "title" fields.
`ErrorResponse` exposes message codes for "type", "title", and "detail", in addition to
message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves
these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
and updates the `ProblemDetail` accordingly.
By default, the message code for the "detail" field is "problemDetail." + the fully
qualified exception class name. Some exceptions may expose additional message codes in
which case a suffix is added to the default message code. The table below lists message
arguments and codes for Spring WebFlux exceptions:
The default strategy for message codes follows the pattern:
`problemDetail.[type|title|detail].[fully qualified exception class name]`
Some `ErrorResponse` may expose more than one message code, typically adding a suffix
to the default message code. The table below lists message codes, and arguments for
Spring WebFlux exceptions:
[[webflux-ann-rest-exceptions-codes]]
[cols="1,1,2", options="header"]
@ -131,9 +132,6 @@ via `MessageSource`. @@ -131,9 +132,6 @@ via `MessageSource`.
|===
By default, the message code for the "title" field is "problemDetail.title." + the fully
qualified exception class name.

27
framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc

@ -71,20 +71,21 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an @@ -71,20 +71,21 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an
[.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-i18n[See equivalent in the Reactive stack]#
It is a common requirement to internationalize error response details, and good practice
to customize the problem details for Spring MVC exceptions. This is supported as follows:
to customize the problem details for Spring WebFlux exceptions. This section describes the
support for that.
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource].
The actual message code value is parameterized with placeholders, e.g.
`+"HTTP method {0} not supported"+` to be expanded from the arguments.
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
"detail" and the "title" fields.
`ErrorResponse` exposes message codes for "type", "title", and "detail", in addition to
message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves
these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
and updates the `ProblemDetail` accordingly.
By default, the message code for the "detail" field is "problemDetail." + the fully
qualified exception class name. Some exceptions may expose additional message codes in
which case a suffix is added to the default message code. The table below lists message
arguments and codes for Spring MVC exceptions:
The default strategy for message codes follows the pattern:
`problemDetail.[type|title|detail].[fully qualified exception class name]`
Some `ErrorResponse` may expose more than one message code, typically adding a suffix
to the default message code. The table below lists message codes, and arguments for
Spring MVC exceptions:
[[mvc-ann-rest-exceptions-codes]]
[cols="1,1,2", options="header"]
@ -171,8 +172,6 @@ arguments and codes for Spring MVC exceptions: @@ -171,8 +172,6 @@ arguments and codes for Spring MVC exceptions:
|===
By default, the message code for the "title" field is "problemDetail.title." + the fully
qualified exception class name.

35
spring-web/src/main/java/org/springframework/web/ErrorResponse.java

@ -64,6 +64,18 @@ public interface ErrorResponse { @@ -64,6 +64,18 @@ public interface ErrorResponse {
*/
ProblemDetail getBody();
/**
* Return a code to use to resolve the problem "type" for this exception
* through a {@link MessageSource}. The type resolved through the
* {@code MessageSource} will be passed into {@link URI#create(String)}
* and therefore must be an encoded URI String.
* <p>By default this is initialized via {@link #getDefaultTypeMessageCode(Class)}.
* @since 6.1
*/
default String getTypeMessageCode() {
return getDefaultTypeMessageCode(getClass());
}
/**
* Return a code to use to resolve the problem "detail" for this exception
* through a {@link MessageSource}.
@ -109,15 +121,19 @@ public interface ErrorResponse { @@ -109,15 +121,19 @@ public interface ErrorResponse {
}
/**
* Resolve the {@link #getDetailMessageCode() detailMessageCode} and the
* {@link #getTitleMessageCode() titleMessageCode} through the given
* {@link MessageSource}, and if found, update the "detail" and "title"
* fields respectively.
* Use the given {@link MessageSource} to resolve the
* {@link #getTypeMessageCode() type}, {@link #getTitleMessageCode() title},
* and {@link #getDetailMessageCode() detail} message codes, and then use the
* resolved values to update the corresponding fields in {@link #getBody()}.
* @param messageSource the {@code MessageSource} to use for the lookup
* @param locale the {@code Locale} to use for the lookup
*/
default ProblemDetail updateAndGetBody(@Nullable MessageSource messageSource, Locale locale) {
if (messageSource != null) {
String type = messageSource.getMessage(getTypeMessageCode(), null, null, locale);
if (type != null) {
getBody().setType(URI.create(type));
}
Object[] arguments = getDetailMessageArguments(messageSource, locale);
String detail = messageSource.getMessage(getDetailMessageCode(), arguments, null, locale);
if (detail != null) {
@ -132,6 +148,17 @@ public interface ErrorResponse { @@ -132,6 +148,17 @@ public interface ErrorResponse {
}
/**
* Build a message code for the "type" field, for the given exception type.
* @param exceptionType the exception type associated with the problem
* @return {@code "problemDetail.type."} followed by the fully qualified
* {@link Class#getName() class name}
* @since 6.1
*/
static String getDefaultTypeMessageCode(Class<?> exceptionType) {
return "problemDetail.type." + exceptionType.getName();
}
/**
* Build a message code for the "detail" field, for the given exception type.
* @param exceptionType the exception type associated with the problem

17
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityExceptionHandlerTests.java

@ -18,6 +18,7 @@ package org.springframework.web.reactive.result.method.annotation; @@ -18,6 +18,7 @@ package org.springframework.web.reactive.result.method.annotation;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@ -130,29 +131,33 @@ public class ResponseEntityExceptionHandlerTests { @@ -130,29 +131,33 @@ public class ResponseEntityExceptionHandlerTests {
Locale locale = Locale.UK;
LocaleContextHolder.setLocale(locale);
String type = "https://example.com/probs/unsupported-content";
String title = "Media type is not valid or not supported";
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage(
ErrorResponse.getDefaultDetailMessageCode(UnsupportedMediaTypeStatusException.class, null), locale,
"Content-Type {0} not supported. Supported: {1}");
messageSource.addMessage(
ErrorResponse.getDefaultTitleMessageCode(UnsupportedMediaTypeStatusException.class), locale,
"Media type is not valid or not supported");
ErrorResponse.getDefaultTitleMessageCode(UnsupportedMediaTypeStatusException.class), locale, title);
messageSource.addMessage(
ErrorResponse.getDefaultTypeMessageCode(UnsupportedMediaTypeStatusException.class), locale, type);
this.exceptionHandler.setMessageSource(messageSource);
Exception ex = new UnsupportedMediaTypeStatusException(MediaType.APPLICATION_JSON,
List.of(MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_XML));
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")
.acceptLanguageAsLocales(locale).build());
MockServerWebExchange exchange = MockServerWebExchange.from(
MockServerHttpRequest.get("/").acceptLanguageAsLocales(locale).build());
ResponseEntity<?> responseEntity = this.exceptionHandler.handleException(ex, exchange).block();
ProblemDetail body = (ProblemDetail) responseEntity.getBody();
assertThat(body.getDetail()).isEqualTo(
"Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]");
assertThat(body.getTitle()).isEqualTo(
"Media type is not valid or not supported");
assertThat(body.getTitle()).isEqualTo(title);
assertThat(body.getType()).isEqualTo(URI.create(type));
}
@Test

15
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.beans.PropertyChangeEvent;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -164,14 +165,18 @@ public class ResponseEntityExceptionHandlerTests { @@ -164,14 +165,18 @@ public class ResponseEntityExceptionHandlerTests {
Locale locale = Locale.UK;
LocaleContextHolder.setLocale(locale);
String type = "https://example.com/probs/unsupported-content";
String title = "Media type is not valid or not supported";
try {
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage(
ErrorResponse.getDefaultDetailMessageCode(HttpMediaTypeNotSupportedException.class, null), locale,
"Content-Type {0} not supported. Supported: {1}");
messageSource.addMessage(
ErrorResponse.getDefaultTitleMessageCode(HttpMediaTypeNotSupportedException.class), locale,
"Media type is not valid or not supported");
ErrorResponse.getDefaultTitleMessageCode(HttpMediaTypeNotSupportedException.class), locale, title);
messageSource.addMessage(
ErrorResponse.getDefaultTypeMessageCode(HttpMediaTypeNotSupportedException.class), locale, type);
this.exceptionHandler.setMessageSource(messageSource);
@ -181,8 +186,8 @@ public class ResponseEntityExceptionHandlerTests { @@ -181,8 +186,8 @@ public class ResponseEntityExceptionHandlerTests {
ProblemDetail body = (ProblemDetail) entity.getBody();
assertThat(body.getDetail()).isEqualTo(
"Content-Type application/json not supported. Supported: [application/atom+xml, application/xml]");
assertThat(body.getTitle()).isEqualTo(
"Media type is not valid or not supported");
assertThat(body.getTitle()).isEqualTo(title);
assertThat(body.getType()).isEqualTo(URI.create(type));
}
finally {
LocaleContextHolder.resetLocaleContext();

Loading…
Cancel
Save