backend / graphql
Gestion des erreurs GraphQL
Explication
Ce que vous allez apprendre
- Comprendre pourquoi une réponse GraphQL en erreur renvoie quand même le code HTTP 200
- Lire la structure du tableau
errorset son champextensions.code - Lever des erreurs typées et exploitables côté client, avec un code d'erreur explicite
- Comprendre comment une erreur sur un champ non-nullable "remonte" dans la réponse
- Éviter de fuiter des détails techniques internes (stack trace, message d'exception brut)
Dans quel contexte ?
Un client mobile appelle une mutation creerProduit sans être authentifié. Il doit pouvoir distinguer, de façon fiable et automatisée (pas en analysant un texte libre), le cas "authentification requise" du cas "produit introuvable" ou "erreur serveur imprévue", pour afficher le bon écran à l'utilisateur (redirection vers la connexion, message d'erreur générique, ou retry automatique).
D'abord, une surprise pour qui vient de REST
En REST, une erreur se traduit généralement par un code HTTP différent de 200 (404, 401, 500...). En GraphQL, la réponse HTTP reste presque toujours 200, même en cas d'erreur, car une seule requête peut mélanger des champs qui réussissent et d'autres qui échouent : il n'existe pas UN code de statut unique pour représenter ce mélange. C'est le tableau errors du corps JSON qu'il faut inspecter, pas le code HTTP.
Une fois cette idée acceptée, il faut regarder comment une erreur est structurée
Chaque élément du tableau errors porte un message lisible par un humain, un path qui indique quel champ a échoué, et des extensions libres où le serveur peut ajouter un code machine-readable comme NOT_FOUND ou UNAUTHENTICATED. C'est ce code, et non le message en texte libre, que le client doit utiliser pour adapter son comportement.
| Code d'extension courant | Signification | Réaction typique côté client |
|---|---|---|
UNAUTHENTICATED | Utilisateur non connecté | Rediriger vers l'écran de connexion |
FORBIDDEN | Connecté mais sans les droits | Afficher un message de permission refusée |
NOT_FOUND | Ressource inexistante | Afficher une page ou un état "introuvable" |
INTERNAL_SERVER_ERROR | Erreur imprévue côté serveur | Message générique, éventuellement un retry |
Ensuite, un mécanisme moins intuitif : la remontée du null
Si un champ marqué non-nullable (categorie: Produit! par exemple) échoue pendant son exécution, GraphQL ne peut pas simplement mettre null à sa place puisque le contrat du schéma l'interdit. La règle est alors de faire remonter le null jusqu'au premier ancêtre nullable dans la réponse — potentiellement en effaçant des données par ailleurs valides.
Piège fréquent
Marquer un champ ! alors qu'il peut légitimement échouer (par exemple une relation optionnelle vers un service externe) peut faire disparaître toute une branche de données valides à cause d'une seule erreur ponctuelle sur un sous-champ. C'est un rappel direct de la leçon sur la nullabilité : réfléchis honnêtement à ce qui peut réellement échouer avant de marquer un champ comme obligatoire.
Il reste un point de sécurité essentiel
En développement, il est tentant de laisser remonter le message d'exception brut ou la stack trace Python/JavaScript dans la réponse GraphQL, pour gagner du temps de debug. En production, c'est une fuite d'information potentielle (structure interne, noms de tables, chemins de fichiers). La plupart des serveurs GraphQL (Strawberry, Apollo Server) permettent de masquer automatiquement ces détails et de renvoyer un message générique aux erreurs non prévues explicitement.
Bonne pratique
Distingue toujours, dans ton code serveur, les erreurs "attendues" que tu lèves volontairement avec un code explicite (authentification, permission, validation) des erreurs "imprévues" qui remontent d'une exception non gérée. Seules les premières devraient exposer un message utile au client ; les secondes doivent être journalisées côté serveur et masquées côté client.
Maintenant que tu sais gérer proprement les erreurs, la leçon suivante s'attaque à un problème de performance très classique dès qu'on résout des relations imbriquées : le fameux problème N+1, et sa solution standard, DataLoader.
Commandes & code
Gestion des erreurs GraphQL
// GraphQL retourne TOUJOURS HTTP 200, même en cas d'erreur -- les erreurs sont dans le corps
{
"data": { "produit": null },
"errors": [
{
"message": "Produit introuvable",
"path": ["produit"],
"extensions": {
"code": "NOT_FOUND",
"produitId": "999"
}
}
]
}// Apollo Server : lever une erreur typée avec un code d'extension exploitable côté client
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
produit: async (_parent, { id }, context) => {
const produit = await context.dataSources.produits.trouverParId(id);
if (!produit) {
throw new GraphQLError('Produit introuvable', {
extensions: { code: 'NOT_FOUND', produitId: id },
});
}
return produit;
},
},
Mutation: {
creerProduit: async (_parent, { input }, context) => {
if (!context.utilisateur) {
throw new GraphQLError('Authentification requise', {
extensions: { code: 'UNAUTHENTICATED', http: { status: 401 } },
});
}
if (!context.utilisateur.peutCreerProduit()) {
throw new GraphQLError('Permission refusée', {
extensions: { code: 'FORBIDDEN', http: { status: 403 } },
});
}
return context.dataSources.produits.creer(input);
},
},
};# Erreur partielle : GraphQL peut retourner des données ET des erreurs simultanément
# si un champ non-nullable échoue, l'erreur "remonte" jusqu'au premier parent nullable
query {
produit(id: "42") {
nom
categorie { # si categorie! échoue, null remonte jusqu'à "produit" (nullable)
nom
}
}
}# Strawberry : masquer les détails d'erreurs internes en production (sécurité)
import strawberry
from strawberry.extensions import Extension
class MasquerErreursInternes(Extension):
def on_operation(self):
yield
result = self.execution_context.result
if result and result.errors:
for erreur in result.errors:
if not isinstance(erreur.original_error, ErreurMetier):
# Ne jamais exposer la stack trace ou le message d'exception brut au client
erreur.message = "Une erreur interne est survenue"
schema = strawberry.Schema(query=Query, extensions=[MasquerErreursInternes])Résumé
- Le code HTTP reste 200 : c'est le tableau
errorsdu corps de réponse qu'il faut inspecter. extensions.code(NOT_FOUND, UNAUTHENTICATED, FORBIDDEN...) donne au client un moyen fiable de réagir par type d'erreur.- Un champ non-nullable qui échoue fait remonter
nulljusqu'au premier ancêtre nullable dans la réponse. - Ne jamais exposer les messages d'exception bruts ou les stack traces en production : risque de fuite d'information.
Exercices pratiques
Mission : outiller un client mobile pour réagir précisément aux erreurs GraphQL
Objectif : Concevoir des erreurs typées avec des codes exploitables côté client, et raisonner sur la propagation du null vers les champs non-nullables.
Contexte
L'équipe mobile veut afficher un écran de connexion si une mutation échoue par manque d'authentification, un message générique si le produit demandé n'existe pas, et un état de retry si le serveur plante de façon imprévue. Elle refuse de parser le texte libre du champ message pour distinguer ces trois cas, car ce texte peut changer sans préavis.