Chapitre 3: Gestion des Erreurs HTTP
Introduction
Une API REST professionnelle doit gérer les erreurs de manière claire et cohérente, en renvoyant les bons codes de statut HTTP et des messages d'erreur explicites.
ResponseEntity
Qu'est-ce que ResponseEntity?
ResponseEntity est une classe Spring qui permet de contrôler complètement la réponse HTTP:
- Code de statut HTTP
- En-têtes HTTP
- Corps de la réponse
Syntaxe de Base
ResponseEntity<TypeRetour> methodName() {
return ResponseEntity
.status(HttpStatus.OK)
.body(data);
}
Codes de Statut HTTP Principaux
Tableau Récapitulatif
| Code | Nom | Usage | Exemple |
|---|---|---|---|
| 200 | OK | Succès général | GET réussi |
| 201 | Created | Ressource créée | POST réussi |
| 204 | No Content | Succès sans contenu | DELETE réussi |
| 400 | Bad Request | Requête invalide | Validation échouée |
| 404 | Not Found | Ressource introuvable | GET id inexistant |
| 500 | Internal Server Error | Erreur serveur | Exception inattendue |
Exemples Pratiques avec ResponseEntity
200 OK - Récupération Réussie
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}
// Équivalent à:
return ResponseEntity.status(HttpStatus.OK).body(product);
201 Created - Création Réussie
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// Ou avec URI:
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
204 No Content - Suppression Réussie
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
404 Not Found - Ressource Introuvable
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
try {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
} catch (ProductNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
Gestion Globale des Exceptions avec @ControllerAdvice
Exception Personnalisée
package com.example.demo.exception;
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product not found with id: " + id);
}
}
Gestionnaire Global
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.NOT_FOUND.value());
body.put("error", "Not Found");
body.put("message", ex.getMessage());
body.put("path", request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGlobalException(
Exception ex, WebRequest request) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
body.put("error", "Internal Server Error");
body.put("message", ex.getMessage());
body.put("path", request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
Utilisation dans le Service
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
}
Réponse JSON
{
"timestamp": "2025-11-28T10:30:45",
"status": 404,
"error": "Not Found",
"message": "Product not found with id: 123",
"path": "/api/products/123"
}
Classe ErrorResponse Personnalisée
package com.example.demo.exception;
import java.time.LocalDateTime;
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
public ErrorResponse(int status, String error, String message, String path) {
this.timestamp = LocalDateTime.now();
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}
// Getters et setters
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
Utilisation
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
Architecture Complète: Controller + Service + Exception
1. Contrôleur
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@Valid @RequestBody Product product) {
Product updated = productService.updateProduct(id, product);
return ResponseEntity.ok(updated);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
2. Service
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
public Product createProduct(Product product) {
if (product.getPrice() < 0) {
throw new InvalidProductException("Le prix ne peut pas être négatif");
}
return productRepository.save(product);
}
public Product updateProduct(Long id, Product productDetails) {
Product product = getProductById(id);
product.setName(productDetails.getName());
product.setPrice(productDetails.getPrice());
product.setQuantity(productDetails.getQuantity());
return productRepository.save(product);
}
public void deleteProduct(Long id) {
Product product = getProductById(id);
productRepository.delete(product);
}
}
3. Exceptions Personnalisées
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product not found with id: " + id);
}
}
public class InvalidProductException extends RuntimeException {
public InvalidProductException(String message) {
super(message);
}
}
4. Gestionnaire Global
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(404, "Not Found", ex.getMessage(),
request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(InvalidProductException.class)
public ResponseEntity<ErrorResponse> handleInvalidProduct(
InvalidProductException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(400, "Bad Request", ex.getMessage(),
request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(
Exception ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(500, "Internal Server Error", ex.getMessage(),
request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Bonnes Pratiques
DO (À Faire)
✅ Utiliser ResponseEntity pour toutes les réponses
✅ Renvoyer les bons codes HTTP
✅ Créer des exceptions personnalisées
✅ Utiliser @ControllerAdvice pour la gestion globale
✅ Fournir des messages d'erreur clairs
DON'T (À Éviter)
❌ Retourner toujours 200 OK
❌ Exposer des traces d'erreur techniques au client
❌ Gérer les exceptions dans chaque controller
❌ Utiliser Exception générique
Résumé du Chapitre
Points Clés
- ResponseEntity pour contrôler les réponses HTTP
- Codes de statut appropriés (200, 201, 204, 400, 404, 500)
- @ControllerAdvice pour gestion globale des erreurs
- Exceptions personnalisées pour clarté du code
- ErrorResponse pour structure cohérente