Cette RFC permet d’uniformiser la façon de répondre en cas d’erreur, avec un contenu plus riche que le status code. Le contenu des réponse peut être en JSON ou XML ; je vais me focaliser sur la variante JSON.
RFC 7807
Media type: application/problem+json
attributs:
-
type (URI): type du problème, référence à une documentation lisible par un humain
-
title (texte): titre court, lisible par un humain, éventuellement traduit
-
status (nombre): status code HTTP
-
detail (texte): description détaillée
-
instance (URI): référence pour l’occurence du problème
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
}
La spécification est souple pour l’ajout d’attributs supplémentaires.
Intégration dans Spring
Avec Spring Boot 2 et Spring Framework 3, l’intégration se faisait via la librairie tierce Zalando Problem. Celle-ci n’est pas compatible avec Spring Boot 3. Spring Framework 6 (et Spring Boot 3) supporte nativement des fonctionnalités similaires.
ExceptionHandler par défaut
Pour gérer les exceptions, on fait des @ExceptionHandler
.
Ici, on fait la même chose.
Pour faciliter le travail, on peut implémenter un gestionnaire d’exception qui hérite de ResponseEntityExceptionHandler
.
@RestControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
}
La classe de base gère une bonne vingtaine d’exceptions.
On peut ajouter des informations personnalisées en redéfinissant la méthode createResponseEntity(…)
, ou handleExceptionInternal(…)
.
L’idée c’est enrichir le corps de la réponse, qui est probablement de type ProblemDetail.
-
ProblemDetail
-
type: URI
-
title: String
-
status: int
-
detail: String
-
instance: URI
-
properties: Map<String,Object>
-
protected ResponseEntity<Object> createResponseEntity(
@Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
if (body instanceof ProblemDetail problem) {
problem.setInstance(URI.create(servletRequest.getRequestURI()));
problem.setProperty("cid", CorrelationIdInterceptor.getCorrelationId());
}
return new ResponseEntity<>(problem, headers, statusCode);
}
Erreurs personnalisées
On peut aussi ajouter des handlers pour nos propres exceptions dans la même classe.
@RestControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler
public ProblemDetail handleTechnicalException(TechnicalException exception, WebRequest request) {
ProblemDetail body = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, exception.getMessage());
return handleExceptionInternal(exception, body, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
}
@ExceptionHandler
public ProblemDetail handleBusinessException(BusinessException exception, WebRequest request) {
ProblemDetail body = ProblemDetail.forStatusAndDetail(exception.getStatus(), exception.getMessage());
return handleExceptionInternal(exception, body, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
}
}
i18n
ErrorResponse
sert à ça, avec l’utilisation d’un MessageSource
.
Pas mal d’exceptions de Spring implémentent ErrorResponse
.
-
ErrorResponse
-
getBody(): ProblemDetail
-
getTitleMessageCode(): String
-
getDetailMessageCode(): String
-
getDetailMessageArguments(): String
-
getHeaders(): HttpHeaders
-
getStatusCode(): HttpStatusCode
-
Au lieu de spécifier un titre et une description détaillées, on passe des codes, du type 'problemDetail.title.unknown-product' et 'problemDetail.detail.unknown-product'. La recherche des vrais messages se fait pas l’intermédiaire d’un MessageSource, avec la locale comme paramètre.
La classe ErrorResponseException
implémente cette interface et peut servir de base aux exceptions personnalisées.
Dans ResponseEntityExceptionHandler
, les instances de l’interface sont utilisées comme intermédiaire pour construrire une instance de ProblemDetail
.
Mais une méthode annotée en @ExceptionHandler
peut aussi renvoyer directement un objet de type ErrorResponse
.
Ça peut faire du code très léger!
@ExceptionHandler
public ErrorResponse handleCustomException(CustomException exception) {
return exception;
}
L’instance de MessageSource
est enregistrée dans ResponseEntityExceptionHandler
.
@RestControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
@PostConstruct
public void init() {
ReloadableResourceBundleMessageSource messageSource
= new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:problem");
messageSource.setDefaultEncoding("UTF-8");
this.setMessageSource(messageSource);
}
...
}
Il ne reste plus qu’à ajouter les messages dans les fichiers properties…
Solutions alternatives
Il existe des solutions alternatives sous forme de librairies tierces.
Zalando Problems for Spring
C’est la solution historique, qui était déjà utilisable avec Spring Boot 2. Après quelques hésitation, elle support Spring Boot 3.
Spting Boot Problem Handler
C’est tout nouveau, pour Spring Boot 3. Je n’ai pas encore testé.