Si chaque controller gère ses erreurs :
Objectif : un seul format, partout.
{ "timestamp": "2025-12-31T09:00:00", "status": 400, "error": "BAD_REQUEST", "message": "Tatouage déjà utilisé : T123", "path": "/api/chiens" }
Pourquoi c’est important ?
public record ErrorResponse( LocalDateTime timestamp, int status, String error, String message, String path, Map<String, String> details ) {}
details sert pour la validation (champ et erreur).
details
@RestControllerAdvice public class GlobalExceptionHandler { private ErrorResponse base(HttpStatus status, String message, HttpServletRequest req) { return new ErrorResponse(LocalDateTime.now(), status.value(), status.name(), message, req.getRequestURI(), Map.of()); } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity<ErrorResponse> illegalArg(IllegalArgumentException ex, HttpServletRequest req) { return ResponseEntity.badRequest().body(base(HttpStatus.BAD_REQUEST, ex.getMessage(), req)); } @ExceptionHandler(IllegalStateException.class) public ResponseEntity<ErrorResponse> illegalState(IllegalStateException ex, HttpServletRequest req) { return ResponseEntity.status(HttpStatus.CONFLICT).body(base(HttpStatus.CONFLICT, ex.getMessage(), req)); } }
Quand @Valid échoue, Spring lance MethodArgumentNotValidException.
@Valid
MethodArgumentNotValidException
@ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> validation(MethodArgumentNotValidException ex, HttpServletRequest req) { Map<String, String> details = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(err -> details.put(err.getField(), err.getDefaultMessage())); ErrorResponse res = new ErrorResponse(LocalDateTime.now(), 400, "BAD_REQUEST", "Validation échouée", req.getRequestURI(), details); return ResponseEntity.badRequest().body(res); }
{ "message": "Validation échouée", "details": { "email": "doit être une adresse email valide", "nom": "ne doit pas être vide" } }
@Test void invalid_payload_returns_details() throws Exception { mockMvc.perform(post("/api/proprietaires") .contentType(MediaType.APPLICATION_JSON) .content("{"nom":"","prenom":"","email":"pasunemail"}")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.details.email").exists()); }
NotFoundException