Skip to content

Commit

Permalink
Add ProblemDetail "type" message code
Browse files Browse the repository at this point in the history
  • Loading branch information
rstoyanchev committed Jun 14, 2023
1 parent 53828cb commit 9c7b5cb
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 44 deletions.
Expand Up @@ -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"]
Expand Down Expand Up @@ -131,9 +132,6 @@ via `MessageSource`.

|===

By default, the message code for the "title" field is "problemDetail.title." + the fully
qualified exception class name.




Expand Down
Expand Up @@ -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"]
Expand Down Expand Up @@ -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.



Expand Down
Expand Up @@ -64,6 +64,18 @@ default HttpHeaders getHeaders() {
*/
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}.
Expand Down Expand Up @@ -109,15 +121,19 @@ default String getTitleMessageCode() {
}

/**
* 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) {
Expand All @@ -132,6 +148,17 @@ default ProblemDetail updateAndGetBody(@Nullable MessageSource messageSource, Lo
}


/**
* 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
Expand Down
Expand Up @@ -18,6 +18,7 @@


import java.lang.reflect.Method;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -130,29 +131,33 @@ void errorResponseProblemDetailViaMessageSource() {
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
Expand Down
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -164,14 +165,18 @@ public void errorResponseProblemDetailViaMessageSource() {
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);

Expand All @@ -181,8 +186,8 @@ public void errorResponseProblemDetailViaMessageSource() {
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();
Expand Down

0 comments on commit 9c7b5cb

Please sign in to comment.