Retour au cours

backend / graphql

Gestion des erreurs GraphQL

Leçon 91 exercice

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 errors et son champ extensions.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 courantSignificationRéaction typique côté client
UNAUTHENTICATEDUtilisateur non connectéRediriger vers l'écran de connexion
FORBIDDENConnecté mais sans les droitsAfficher un message de permission refusée
NOT_FOUNDRessource inexistanteAfficher une page ou un état "introuvable"
INTERNAL_SERVER_ERRORErreur imprévue côté serveurMessage 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

json
// 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"
      }
    }
  ]
}
javascript
// 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);
    },
  },
};
graphql
# 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
    }
  }
}
python
# 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 errors du 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 null jusqu'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

1 disponible
1

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.

Résoudre l’exercice →