Retour au cours

backend / fastapi

Erreurs HTTP personnalisées

Leçon 61 exercice

Explication

Ce que vous allez apprendre

  • Lever une HTTPException avec 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.

ApprocheCouplage à FastAPIRéutilisable hors contexte web
HTTPException levée dans l'endpointFortNon
Exception métier + exception_handlerFaibleOui

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

python
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
python
# 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}
python
# 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")
python
# 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
python
# 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é

  • HTTPException convient 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 Exception garantit qu'aucune erreur inattendue ne fuite de stack trace au client.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →