CHAPITRE 10 : VALIDATION ET GESTION DES ERREURS

Une API doit refuser proprement les donnees invalides, et expliquer pourquoi. C'est ce qui distingue une API utilisable d'une API frustrante.

1. Ajouter la validation

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2. Annoter le DTO

public record InscriptionDTO(

    @NotBlank(message = "Le nom est obligatoire")
    @Size(min = 3, max = 80, message = "Le nom doit compter entre 3 et 80 caracteres")
    String nom,

    @NotBlank(message = "Le matricule est obligatoire")
    @Pattern(regexp = "[0-9]{2}[A-Z][0-9]{3}",
             message = "Format attendu : 19A123")
    String matricule,

    @Email(message = "Adresse e-mail invalide")
    String email,

    @NotNull(message = "La filiere est obligatoire")
    @Positive(message = "Identifiant de filiere invalide")
    Long filiereId,

    @Min(value = 0,  message = "La moyenne ne peut etre negative")
    @Max(value = 20, message = "La moyenne ne peut depasser 20")
    double moyenne
) { }

Les annotations courantes

AnnotationVerifie
@NotNullNon nul
@NotBlankNon nul et non vide (chaines)
@NotEmptyNon vide (collections)
@SizeLongueur
@Min / @MaxBornes numeriques
@EmailFormat d'adresse
@PatternExpression reguliere
@Past / @FutureDates

Distinguez bien @NotNull et @NotBlank : une chaine composee d'espaces passe le premier test, pas le second.

3. Declencher la validation

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public EtudiantDTO inscrire(@Valid @RequestBody InscriptionDTO dto) {
    return mapper.versDTO(service.inscrire(dto));
}

Sans @Valid, rien n'est verifie : les annotations du DTO restent lettre morte. C'est l'oubli le plus frequent.

4. Le probleme de la reponse par defaut

Spring renvoie bien un 400, mais accompagne d'une longue trace technique illisible pour le client. Nous allons la remplacer.

5. Centraliser la gestion des erreurs

@RestControllerAdvice
public class GestionnaireErreurs {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> champsInvalides(MethodArgumentNotValidException ex) {

        Map<String, String> erreurs = new HashMap<>();

        ex.getBindingResult().getFieldErrors().forEach(e ->
            erreurs.put(e.getField(), e.getDefaultMessage())
        );

        return Map.of(
            "statut",  400,
            "message", "Donnees invalides",
            "erreurs", erreurs
        );
    }

    @ExceptionHandler(EtudiantIntrouvableException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Map<String, Object> introuvable(EtudiantIntrouvableException ex) {
        return Map.of("statut", 404, "message", ex.getMessage());
    }

    @ExceptionHandler(MatriculeDejaUtiliseException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public Map<String, Object> conflit(MatriculeDejaUtiliseException ex) {
        return Map.of("statut", 409, "message", ex.getMessage());
    }
}

Le client recoit desormais une reponse claire :

{
  "statut": 400,
  "message": "Donnees invalides",
  "erreurs": {
    "nom": "Le nom doit compter entre 3 et 80 caracteres",
    "matricule": "Format attendu : 19A123"
  }
}

Un @RestControllerAdvice s'applique a tous les controleurs : la gestion des erreurs est ecrite une fois pour toute l'application.

6. Choisir le bon code

SituationCode
Donnees invalides400 Bad Request
Non authentifie401 Unauthorized
Authentifie mais non autorise403 Forbidden
Ressource inexistante404 Not Found
Conflit (doublon)409 Conflict
Erreur interne500 Internal Server Error

La confusion 401 / 403 est frequente : 401 signifie « je ne sais pas qui vous etes », 403 « je sais qui vous etes, mais vous n'avez pas le droit ».

7. Ne jamais exposer la trace technique

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> erreurInattendue(Exception ex) {

    log.error("Erreur inattendue", ex);   // trace complete dans les journaux

    return Map.of(
        "statut",  500,
        "message", "Une erreur est survenue. Veuillez reessayer."
    );
}

Une trace exposee publiquement revele votre arborescence, vos bibliotheques et parfois des fragments de requetes SQL. Elle appartient aux journaux, pas a la reponse.

A retenir

  • Sans @Valid, les annotations ne font rien
  • @NotBlank rejette les chaines d'espaces, pas @NotNull
  • @RestControllerAdvice centralise les erreurs
  • 401 = non authentifie, 403 = non autorise
  • La trace technique va dans les journaux, jamais dans la reponse

Exercices d'application

Exercice 1. Valider une inscriptionAjoutez les contraintes a votre DTO et testez avec des donnees invalides. Verifiez que vous recevez bien un 400.
Exercice 2. Reponses propresEcrivez un @RestControllerAdvice qui renvoie un JSON lisible listant chaque champ en erreur avec son message.
Exercice 3. Les bons codesFaites renvoyer 404 pour un identifiant inexistant et 409 pour un matricule deja utilise. Verifiez avec curl -i.

Cherchez avant de regarder la solution : c'est en butant sur les difficultes que l'on progresse.

Questions / Réponses

Aucune question. Soyez le premier à poser une question.
★ ★ ★ ★ ★
2 votes. Moyenne 5 sur 5.

Ajouter un commentaire

Anti-spam