Comment définir et signaler les erreurs d'APIs

Lorsque vous travaillez avec des API, il est inévitable que vous rencontrez une erreur à un moment donné. Même si vous suivez la documentation à la lettre et copiez et collez des exemples de code, il y a toujours quelque chose qui va foirer.
Dans cet article, nous allons vous montrer comment:
- Concevoir les erreurs d'APIs afin d'améliorer l'expérience des développeurs qui les utilisent.
- Spring Framework permet de gérer et signaler les erreurs de manière élégante.

Retourner le bon code d'erreur
La première étape pour signaler une erreur d'API est de retourner le bon statut HTTP. Comme expliqué dans cet article, les statuts important à connaitre sont les suivants:
2XX
- Tout est OK4XX
- Le client a foiré5XX
- Le serveur a foiré

Soyez descriptif
Les statuts HTTP sont bons pour les machines; pensez aux développeurs qui vont utiliser votre API. En plus du statut, il faut retourner un message pour permettre de mieux comprendre le problème.
Voici un exemple d'erreur d'une API de paiement:
HTTP/1.1 403 Forbidden Content-Type: application/json { "code": "urn:error:payment:not-enough-funds", "message": "You do not have enough funds in your account", "downstreamErrorCode": "13602" "requestId": "db6e760c-76db-4da3-bb66-3cd1de52731d", "data": { "accountId": "1132094", "balance": 300 } }
Voici un exemple de requête invalide:
HTTP/1.1 400 Bad Request Content-Type: application/json { "code": "urn:error:global:validation-error", "message": "Your request is not valid", "requestId": "db6e760c-76db-4da3-bb66-3cd1de52731d", "parameters":[ { "name": "amount", "reason": "The amount should be greater than 1000" }, { "name": "email", "reason": "The email is missing" } ] }
Voici un exemple d'une erreur inattendu sur le serveur:
HTTP/1.1 500 Internal Server Error Content-Type: application/json { "code": "urn:error:global:unexpected-error", "message": "An unexpected error has occurred", "requestId": "db6e760c-76db-4da3-bb66-3cd1de52731d", }
Les informations du message d'erreur doivent inclure:
code
: L'identifiant du problème.message
: La description sommaire du problème.downstreamErrorCode
: (Optionnel) Il s'agit du code d'erreur qui provient d'une ressource ou système tierce qui a causé le problème. Par exemple, si l'API utilise PayPal pour faire des transferts de fonds, lorsqu'il n'y a pas assez d'argent dans le compte débiteur, PayPal va retourner le code13602
. , qui va être retourné dansdownstreamErrorCode
.requestId
: (Optionnel) L'identifiant de la requête, qui permet d'avoir une traçabilité entre l'erreur et les requêtes traitées.parameters
: (Optionnel) La liste des informations de la requête qui ont causé le problème.data
(Optionnel) Un dictionnaire de données contenant des informations additionnelles pour permettre aux développeurs de mieux comprendre le contexte de l'erreur.
Mise en pratique
1. Définir le message d'erreur
Commencer par définir la message que votre API va retourner en cas d'erreur: ErrorResponse
class ErrorResponse { error: Error } class Error { code: String, message: String, downstreamErrorCode: String, requestId: String, data: Map<String, String>, parameters: List<Parameter> } class Parameter { name: String, reason: String, }
2. Définir les exceptions
Créer la hiérarchie d'exceptions, en créeant une exception pour chacun des statuts HTTP que votre API va utiliser.
class HTTPException { status: Int, error: Error } class BadRequestException: HTTPException(status=400) class ForbiddenException: HTTPException(status=403) class NotFoundException: HTTPException(status=404) class InternalErrorException: HTTPException(status=500)
3. Créer un gestionnaire d'exception
Créer la classe qui va gérer toutes les exceptions en retournant le message d'erreur approprié.
class ExceptionHandler{ fun handle(ex: Exception, response: HTTPResponse){ if (ex is HttpException){ response.sendError(ex.status, ErrorResponse(ex.error)) } else { response.sendError(500, createDefaultError()) } } private fun createDefaultError(): ErrorResponse { ... } }
4. Intégrez le tout
Intégrez le tout dans la classe qui va traiter la requête de votre API. Lors du traitement de chaque requête:
- Validez la requête reçue.
- Exécutez la requête. En cas d'erreur, lancer l'exception appropriée.
- Utiliser
ExceptionHandler
pour traiter toutes les exceptions lancées.
class TransferEndpoint { val exceptionHandler: ExceptionHandler val payal: PaypalService // POST /transfers fun transfer(request: TransferRequest, response: HTTPResponse): TransferResponse{ try { validate(request) return execute(request) } catch(ex: Exception) { exceptionHandler.handle(ex, HTTP Response) } } private fun validate(request: TransferRequest){ val errors: List<Error> if (request.email == null) errors.add(Error(...)) if (request.amount < 1000) errors.add(Error(...)) if (errors.isNotEmpty) throw BadRequestException(errors) } private fun execute(request: TransferRequest): TransferResponse{ val result = paypal.transfer(request) if (!result.success){ switch(result.error){ case 13602: throw ForbiddenException(...) ... } } ... } } class TransferRequest{ email: String amount: Long } class TransferResponse{ transactionId: String }
Gestion d'exception avec Spring Framework
Spring Framework permet de simplifier la gestion des erreurs.
Spring Validation
Vous pouvez prendre avantage de Spring Validation afin de valider automatiquement les requêtes de votre API.
1. Ajoutez la dépendance spring-boot-starter-validation
dans le pom.xml
de votre projet.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
2. Utilisez les annotations de la spécification JSR-303 Bean Validation API pour définir les règles de validation de vos messages.
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; public class TransferRequest{ @NotBlank @Email private String email; @Min(1000) private Long amount; }
3. Activez la validation automatique des requêtes avec l'annotation @Valid
.
@RestController public class TransferEndpoint { @PostMethod("/transfer") TransferResponse transfer(@Valid @RequestBody TransferRequest request) { ... } ... }
Le Gestionnaire d'exceptions
Vous pouvez définir un gestionnaire d'exception avec l'annotation @RestControllerAdvice
. Spring va utiliser l'annotation @ExceptionHandler
pour automatiquement appeler la méthode appropriée pour gérer les exceptions.
Puisque les validations sont faites automatiquement, il est important que le gestionnaire d'exception gère aussi l'exception BindException
qui est lancée par Spring en cas d'erreur de validation.
@RestControllerAdvice class ExceptionHandler{ @ExceptionHandler(BindException.class) ResponseEntity<ErrorResponse> handle(BindException ex){ handle(400, createValidationError(ex)) } @ExceptionHandler(Exception.class) ResponseEntity<ErrorResponse> handle(Exception ex){ if (ex instanceof HttpException){ handle(((HttpException)ex).status, error) } else { handle(((HttpException)ex).status, createDefaultError()) } } private ResponseEntity<ErrorResponse> handle(status: Int, error: Error) { return ResponseEntity .status(status) .body(ErrorResponse(error)) } private Error createDefaultError() { ... } private Error createValidationError(BindException ex) { ... } }
Resultat Final
Le résultat final est un code plus simple et propre:
- Plus de code de validation de requêtes car Spring Validation s'en charge automatiquement.
- Plus de code de gestion d'exception car Spring appelle automatiquement la méthode appropriée du
@RestControllerAdvice
.
public class TransferEndpoint { private PaypalService payal; @PostMethod("/transfer") TransferResponse transfer(@Valid @RequestBody TransferRequest request) { return execute(request) } private fun execute(request: TransferRequest): TransferResponse{ ... } } public class TransferRequest{ @NotBlank @Email private String email; @Min(1000) private Long amount; }
Si vous avez aimé l'article, montrez votre soutien avec un ❤️ et abonnez vous a mon blog ! Votre engagement m’inspire!