backend / fastapi
Erreurs HTTP personnalisées
Explication
Ce que vous allez apprendre
- Lever une
HTTPExceptionavec un code de statut et un détail exploitables - Créer des exceptions métier pures, découplées du protocole HTTP
- Centraliser la traduction exception métier vers réponse HTTP avec
@app.exception_handler - Mettre en place un handler générique comme filet de sécurité en production
- Éviter qu'une stack trace technique ne fuite accidentellement vers un client
Dans quel contexte ?
Le service app/services/order_service.py doit être appelable aussi bien depuis l'API que depuis une tâche planifiée qui traite les commandes en attente. Si ProductNotFoundError était directement une HTTPException, la tâche planifiée (sans aucun contexte HTTP) planterait de façon incompréhensible. En gardant une exception métier pure (ProductNotFoundError(Exception)) et un exception handler séparé qui la traduit en réponse HTTP uniquement côté API, la même logique métier reste utilisable dans les deux contextes.
D'abord, communiquer clairement une erreur
Quand quelque chose se passe mal dans une API, il faut le communiquer clairement au client. Une ressource introuvable ou une règle métier violée doit produire un code HTTP adapté et un message exploitable.
HTTPException est l'outil le plus simple de FastAPI pour ça. Il stoppe immédiatement l'exécution de l'endpoint et renvoie une réponse d'erreur structurée.
Mais cette simplicité a une limite sur une grosse application. Lever une HTTPException directement dans chaque fonction de logique métier crée un couplage fort entre cette logique et FastAPI.
Or la logique métier, comme "un produit n'existe pas", n'a en soi rien à voir avec le protocole HTTP. Elle pourrait tout aussi bien être appelée depuis une tâche planifiée ou un script en ligne de commande.
Une architecture plus mûre introduit donc des exceptions métier PURES. Comme ProductNotFoundError, qui hérite d'Exception normalement, sans rien connaître de FastAPI.
Une fois ces exceptions créées, comment les traduire en réponse HTTP ? C'est le rôle des exception handlers.
Un @app.exception_handler(...) centralise, à UN seul endroit du code, la traduction entre une exception métier et une réponse HTTP cohérente. Le bénéfice est double.
D'une part, la logique métier reste testable et réutilisable en dehors du contexte web. D'autre part, la cohérence des réponses d'erreur est garantie sur toute l'API sans y penser dans chaque endpoint.
Il reste un dernier filet de sécurité à mettre en place, pour le pire des cas. Un handler générique sur Exception, la classe mère de toutes les erreurs Python.
| Approche | Couplage à FastAPI | Réutilisable hors contexte web |
|---|---|---|
HTTPException levée dans l'endpoint | Fort | Non |
Exception métier + exception_handler | Faible | Oui |
Piège à éviter
Sans un handler générique sur Exception, une erreur totalement imprévue, comme un bug ou une division par zéro oubliée, ferait fuiter une stack trace technique potentiellement sensible directement au client — une pratique à ne jamais négliger en production.
Commandes & code
Erreurs HTTP personnalisées
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
@app.get("/products/{id}")
def get_product(id: int):
product = find_product(id)
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Produit {id} introuvable",
)
return product
def find_product(id: int):
return None# Détail structuré (pas juste une string) — utile pour un frontend qui parse l'erreur
@app.post("/orders")
def create_order(quantity: int, stock: int = 5):
if quantity > stock:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"code": "INSUFFICIENT_STOCK",
"message": "Stock insuffisant pour cette commande",
"available": stock,
"requested": quantity,
},
)
return {"ok": True}# Exceptions métier custom — découplées de FastAPI, réutilisables dans la logique métier
class DomainError(Exception):
"""Erreur de base pour toutes les erreurs métier de l'application."""
class ProductNotFoundError(DomainError):
def __init__(self, product_id: int):
self.product_id = product_id
super().__init__(f"Produit {product_id} introuvable")
class InsufficientStockError(DomainError):
def __init__(self, available: int, requested: int):
self.available = available
self.requested = requested
super().__init__("Stock insuffisant")# Exception handlers globaux — traduisent les exceptions métier en réponses HTTP cohérentes
from fastapi import Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(ProductNotFoundError)
async def product_not_found_handler(request: Request, exc: ProductNotFoundError):
return JSONResponse(
status_code=404,
content={"code": "PRODUCT_NOT_FOUND", "message": str(exc), "product_id": exc.product_id},
)
@app.exception_handler(InsufficientStockError)
async def insufficient_stock_handler(request: Request, exc: InsufficientStockError):
return JSONResponse(
status_code=409,
content={
"code": "INSUFFICIENT_STOCK",
"available": exc.available,
"requested": exc.requested,
},
)
# Les services métier lèvent des exceptions Python normales, sans connaître HTTP
def get_product_service(product_id: int):
product = find_product(product_id)
if not product:
raise ProductNotFoundError(product_id)
return product
@app.get("/v2/products/{id}")
def get_product_v2(id: int):
return get_product_service(id) # aucun try/except nécessaire ici# Handler générique pour toute erreur non prévue — filet de sécurité en dernier recours
import logging
logger = logging.getLogger("app")
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
logger.exception("Erreur non gérée sur %s %s", request.method, request.url.path)
return JSONResponse(
status_code=500,
content={"code": "INTERNAL_ERROR", "message": "Une erreur interne est survenue"},
)Résumé
HTTPExceptionconvient pour des erreurs simples et ponctuelles directement dans un endpoint.- Des exceptions métier custom (héritant de
DomainError) découplent la logique métier de FastAPI/HTTP. @app.exception_handler(...)centralise la traduction exception métier -> réponse HTTP, cohérente sur toute l'API.- Un handler générique sur
Exceptiongarantit qu'aucune erreur inattendue ne fuite de stack trace au client.
Exercices pratiques
Mission : la tâche planifiée qui plante à cause de l'API
Objectif : Découpler une exception métier couplée à HTTPException pour qu'elle reste utilisable hors du contexte web, puis sécuriser les erreurs inattendues.
Contexte
Dans app/services/order_service.py, la fonction get_product_service lève directement raise HTTPException(status_code=404, detail="Produit introuvable") quand un produit n'existe pas. Une tâche planifiée dans app/tasks/process_orders.py, qui appelle cette même fonction sans aucun contexte HTTP, plante avec une erreur incompréhensible dès qu'un produit manque.