Add ProblemDetail "type" message code
See gh-30566
This commit is contained in:
@@ -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`.
|
||||
|
||||
|===
|
||||
|
||||
By default, the message code for the "title" field is "problemDetail.title." + the fully
|
||||
qualified exception class name.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|===
|
||||
|
||||
By default, the message code for the "title" field is "problemDetail.title." + the fully
|
||||
qualified exception class name.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -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 {
|
||||
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
|
||||
|
||||
@@ -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 @@
|
||||
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 {
|
||||
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 {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user