Comment définir et signaler les erreurs d'APIs

Pragmatic Nerdz
Nov 7, 2023 - 5 Minutes

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 OK
  • 4XX - 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 code 13602. , qui va être retourné dans downstreamErrorCode .
  • 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)
Receive my Stories your e-mail inbox as soon as I publish them.
Subscribe to my Blog

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:

  1. Validez la requête reçue.
  2. Exécutez la requête. En cas d'erreur, lancer l'exception appropriée.
  3. 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!

Api
HTTP
Gestion D'erreurs
Spring Framework