Débogage des échecs des Prisma Guard : un modèle de diagnostic basé sur les phases
Apprenez à diagnostiquer les échecs de l’API Prisma générée en associant les erreurs à la phase précise — configuration, sélection de l’appelant, validation ou réponse — qui en est responsable.
Commencez la résolution des problèmes en identifiant quelle phase est réellement responsable de la panne.
Les API générées peuvent tomber en panne à plusieurs endroits distincts.
Remarque : les phases de panne décrites ici proviennent de la documentation du projet et d’environnements de reproduction spécifiques, et non de statistiques d’utilisation globales sur une large base d’utilisateurs.
Un routeur peut rejeter sa propre configuration avant même d’envoyer une seule requête. Un mécanisme de validation peut rejeter une forme mal formatée dès sa création. Le routage des appels peut échouer avant même que le hook spécifique à une variante ne s’exécute. La validation des requêtes peut rejeter un corps de données particulier. Une opération ciblée peut échouer simplement parce que le contexte de confiance sur lequel elle dépend n’est pas présent.
En dehors de celles-ci, il existe une catégorie plus délicate : la requête réussit techniquement, mais les arguments Prisma générés, ou la sémantique de la réponse, diffèrent de ce que la logique de l’application avait prévu.
Chacune de ces catégories exige une correction différente ainsi qu’un type de test distinct. Analyser l’intégralité de la chaîne d’erreur est bien moins efficace que de se poser deux questions : quand ce comportement est-il apparu pour la première fois, et quel niveau est capable de le détecter ?
Commencer par une carte des phases
Une requête Prisma générée traverse plusieurs étapes distinctes avant d’être exécutée :
router construction
caller resolution
operation before-hooks
variant before-hooks
guard shape construction
request validation
Prisma argument execution
response transport
Le déroulement précis des opérations liées aux formes peut varier en fonction du fait que celles-ci soient statiques ou dépendent du contexte de exécution, mais cette analyse diagnostique reste un modèle mental utile.
Les échecs au démarrage indiquent des problèmes dans les descripteurs de route. Les échecs à l’étape d’appel renvoient à la logique de sélection des variantes. Des erreurs telles que Invalid query et Invalid data signalent une incohérence entre le corps de la requête et sa structure déclarée. Les échecs liés aux politiques indiquent l’absence de contexte fiable. Et lorsque une requête réussit mais que le résultat est inattendu, il faut ignorer complètement le code d’état.
N’oubliez pas que le texte des erreurs est lié à des versions spécifiques. Les exemples axés sur les gardes mentionnés ici ont été générés avec une combinaison fixe : prisma-guard version 1.33.0, associé à Zod 4.4.3 et Prisma 6.19.3. Les exemples concernant les lectures basées sur HTTP, quant à eux, reposent sur un ensemble séparé : prisma-generator-express 1.64.4 exécuté sur Node 22.14.0 avec PostgreSQL 16.6.
Considérez la formulation exacte d’une erreur comme une preuve spécifique à cette combinaison de versions. Considérez la phase et la cause sous-jacente comme le modèle de débogage qui restera utile par la suite.
Au préalable d’une demande : la configuration ne peut pas constituer un contrat
C’est le mécanisme de construction du routeur qui est chargé de valider les descripteurs d’opération avant tout autre traitement.
Une opération ne peut pas configurer à la fois shape et variants. Un tableau de variantes ne peut pas rester vide. Chaque descripteur de variante doit inclure une forme. Les clés de forme réservées ne peuvent pas être utilisées en même temps comme noms d’appelants.
Ces problèmes sont par nature liés au moment du déploiement. Si le système les détectait et continuait à fonctionner malgré un routeur partiellement configuré, il effacerait silencieusement les limites que l’application était censée respecter.
Une opération qui ne définit ni shape ni variants représente une situation complètement différente : elle est techniquement valide et appelle directement Prisma sans aucune vérification de sécurité. Le fait qu’elle soit acceptable doit être une décision explicite prise lors de l’examen des routes, et non le résultat d’un hasard.
La construction de formes présente son propre ensemble de conditions d’échec. Les combinateurs vides, les projections vides, les prédicats forcés contradictoires, les formes créées de manière incomplète, les structures d’upsert mal formatées, ainsi que les méthodes de masse manquant d’une forme where sont toutes rejetées dès le départ, avant même que les données fournies par le client aient la possibilité d’interagir avec elles de manière non sécurisée.
Une reproduction minimale est particulièrement utile lorsqu’elle sépare la construction des formes de la couche de transport :
const query = guard.query('Plant', 'findMany', {
where: {
name: { contains: true },
},
take: { max: 50, default: 20 },
})
const args = query.parse({
where: {
name: { contains: 'fern' },
},
})
Ce chemin isolé fonctionne bien pour tester le filtrage des lectures, l’ordonnancement, les paramètres de pagination et la plupart des erreurs de construction des schémas. Ce qu’il ne fait pas, c’est effectuer réellement des requêtes via Prisma, appliquer une projection de lecture au niveau du délégué, ou montrer comment se comportent les mutations dans la pratique.
Quelle que soit la solution trouvée, elle doit être intégrée dans la configuration du côté serveur. Aucun ajustement du payload de la requête ne peut corriger un schéma qui est structurellement défectueux dès le départ.
Au préalable du gestionnaire : échec de la sélection de l’appelant
Lorsque des schémas et des variantes nommés sont utilisés, il y a une phase de routage supplémentaire qui s’exécute avant même que la requête n’atteigne le gestionnaire généré.
default. Un appelant est considéré comme inconnu lorsqu’aucun élément ne correspond à ses critères : ni clé exacte, ni motif paramétré, ni valeur par défaut. Deux motifs paramétrés qui se chevauchent ne sont pas résolus en fonction de l’ordre de déclaration ; le système considère alors la situation comme ambiguë et échoue.
resolveVariant, et non des entrées fournies par le client. Donner un nom personnalisé à un en-tête ne rend pas sa valeur fiable.
Les échecs de routage se produisent après l’exécution des hooks avant au niveau de l’opération, mais avant celle des hooks spécifiques à chaque variante. Cette séquence explique un comportement subtil : la logique d’authentification applicable à l’opération dans son ensemble s’exécute toujours, même lorsque aucune variante d’appel ne correspond, tandis que les hooks spécifiques à l’appel ne s’exécutent jamais dans ce cas.
La solution appropriée n’est pas automatiquement de « simplement ajouter une valeur par défaut ». Une valeur par défaut pour l’appelant accepte silencieusement les valeurs manquantes, vides ou non correspondantes. N’ajoutez-en une que si ce comportement de secours est véritablement acceptable dans ces trois scénarios.
Pendant la validation : la requête a dépassé ses limites déclarées
Les erreurs de lecture constatées dans cette configuration indiquent le chemin exact des arguments qui les ont déclenchées.
where signifie que ce champ ne fait pas partie de la structure du filtre. Un champ non reconnu à l’intérieur de select indique que la requête tente d’élargir la projection au-delà de ce qui est autorisé. Une valeur skip rejetée signifie que l’omission de pages n’a jamais été activée pour cette structure. Une erreur sur take peut signifier soit que la valeur demandée dépasse son maximum configuré, soit qu’elle est d’un type scalaire complètement incorrect.
Les outils GET générés sont importants ici, car les arguments de type Prisma ne se transforment pas tous de la même manière lorsqu’ils sont créés manuellement à partir de chaînes de requête. Les valeurs numériques de filtre et les dates se transforment généralement correctement là où c’est pris en charge, mais les valeurs booléennes et les valeurs de pagination transmises sous forme de chaînes peuvent ne pas se transformer correctement. Le choix le plus sûr est d’utiliser l’encodeur généré pour les requêtes GET, ou de recourir au JSON natif via la méthode de lecture basée sur POST.
En revanche, l’écriture de validations suit une structure spécifique à chaque méthode Prisma. Les opérations de création reçoivent un champ data. Les opérations de mise à jour reçoivent à la fois where et data. Les opérations d’upsert reçoivent where, create et update. Une appel de création en lot protégée attend que son entrée soit un tableau.
Les opérations en masse peuvent échouer à deux niveaux distincts. Si le champ where fait défaut dans la structure elle-même, il s’agit d’un problème lié à la construction. Si le corps de la demande en temps de exécution contient techniquement un where mais qu’il ne correspond à aucune condition réelle du côté client, il s’agit alors d’un problème lié à la demande elle-même.
Les erreurs de politique forment à nouveau leur propre catégorie. L’absence d’une racine de portée, ou l’absence de contexte pour une structure dépendante du contexte en temps de exécution, indique toutes deux qu’un élément d’état fiable n’est tout simplement pas présent. Le fait de maintenir le comportement en cas d’absence de portée en mode erreur empêche un contexte manquant de se transformer silencieusement en une requête de niveau supérieur non filtrée.
L’habitude à développer ici est de conserver le chemin exact où quelque chose a échoué. Dire « j’ai reçu un code 400 de la protection » ne vous apporte presque aucune information utile. Dire « le traitement du corps de la requête a tenté d’appeler include.plants.take en dehors du maximum imbriqué configuré » cible directement un nœud spécifique dans le contrat.
Lorsque la protection autorise l’accès : un statut 200 cache encore des risques réels
Prenez un prédicat de niveau supérieur entièrement forcé : il remplace ce que l’client envoie sans laisser de trace visible de cette substitution. Si une forme fixe isPublished à true, un client qui envoie false reçoit néanmoins une réponse de succès, tandis que la requête effectivement exécutée conserve la valeur forcé true.
Les autres champs forcés se comportent de manière inverse : ils rejettent purement et simplement les valeurs fournies par l’client au lieu de les remplacer silencieusement. Comme la force peut se comporter de façon incohérente en fonction du lieu où elle est appliquée, vos tests doivent vérifier qui possède réellement chaque argument, plutôt que d’assumer qu’une seule instance de force() s’applique à tous les champs.
Forcer une condition devient encore plus complexe à l’intérieur d’une clause OR. Une condition imposée en ce lieu est extraitée et transformée en contrainte obligatoire au niveau le plus élevé. Ainsi, une structure qui semble exprimer « soit la condition du client, soit celle du serveur » peut en réalité s’exécuter comme la condition du client combinée au prédicat forcé, en utilisant une logique AND. Si vous avez réellement besoin d’une alternance gérée par le serveur, il vous faut une requête dédiée conçue à cette fin, ou bien appliquer cette contrainte au niveau des politiques de la base de données.
La projection des réponses introduit également une divergence subtile. Lorsqu’un client omet une projection lors d’une lecture protégée, la projection par défaut de la structure est utilisée, mais cette substitution a lieu au moment où le délégué s’exécute réellement, et non lorsque guard.query().parse() est exécuté.
Les mutations ne suivent pas la même règle. Si enforceProjection n’est pas défini, un client qui omet une projection lors d’une mutation ne reçoit absolument aucune clause select injectée, ce qui signifie que le comportement normal de Prisma sans projection prend le dessus.
L’application des règles dans un contexte imbriqué est un autre domaine où il est facile de penser qu’il y a plus de couverture que ce qui existe réellement. Le mécanisme automatique de contexte ne intercepte que les opérations de niveau supérieur qu’il prend explicitement en charge. Il ne s’étend pas aux relations incluses via une projection pour les filtrer de manière récursive. De plus, la racine du contexte elle-même n’est jamais filtrée par son propre marqueur, et tout SQL brut que vous exécutez contourne complètement la couche de contrôle de l’extension.
Aucun de ces comportements n’apparaît si vous ne vérifiez que le code d’état.
Sélectionnez le mécanisme de lecture approprié avant de vous fier à la structure de la réponse
La couche générée intègre trois mécanismes distincts pour fournir les résultats de lecture : des réponses paginées, un transport basé sur POST, et des événements envoyés par le serveur via Express.
findManyPaginated renvoie une structure externe fixe :
type PaginatedResult<T> = {
data: T[]
total: number
hasMore: boolean
}
Le drapeau hasMore est fiable uniquement pour la pagination par décalage progressif associée à une valeur positive de take. Si vous utilisez la pagination par curseur ou une valeur négative de take, vous pouvez toujours obtenir un résultat booléen, mais celui-ci ne présente plus la même garantie. Une valeur de take égale à 0 renvoie zéro ligne ainsi qu’un drapeau de continuation faux, tandis que le comptage total reste inchangé.
Le comptage total suit une logique complètement différente. Le comptage distinct respecte une limite prédéfinie. Une source de comptage précalculée n’est utilisée que lorsque la requête est non filtrée, non protégée et non distincte. Tout filtre dynamique, toute clause de distinction ou tout mécanisme de protection oblige à recourir à un comptage en temps réel effectué au moment de la requête.
Ce recours de secours conserve la précision des résultats, mais il modifie à la fois le coût de l’opération et l’origine du chiffre obtenu. Il convient de considérer la sémantique du total comme une question distincte de celle du découpage des lignes.
Les lectures basées sur POST existent pour gérer la taille et l’encodage des données, et non pour étendre les capacités d’expression du langage de requête :
POST /delivery/paginated
Content-Type: application/json
{"where":{"city":{"equals":"Bangkok"}},"take":20,"skip":0}
Envoyez le corps du message sous forme de JSON natif. Les versions GET et POST d’une même route doivent respecter le même contrat de sécurité. Si un hook modifie le corps de la requête, cette équivalence peut être compromise, car le chemin GET lit les paramètres de recherche déjà analysés plutôt que le corps JSON.
Les événements envoyés par le serveur se produisent au moment où les données arrivent, et non en fonction du type de données reçues. Ce mécanisme n’a de sens que si le client implémente réellement un traitement pour les événements de progression, les événements de succès final, les événements d’échec final, ainsi qu’un chemin de secours.
{"type":"progress","stage":"relations"}
{"type":"field","field":"summary","data":{"total":6}}
{"type":"result","data":{"summary":{"total":6},"deliveries":[]}}
Les événements SSE mis en œuvre manuellement sont des requêtes au niveau de l’application que vous écrivez vous-même, et elles nécessitent un traitement de protection explicite tout comme n’importe quel autre élément. L’inclusion automatique ne couvre que les structures relationnelles documentées et restant dans les limites du planificateur ; tout ce qui en dépasse recourt au mécanisme de fallback configuré. De plus, les hooks générés après exécution ne constituent pas un moyen fiable pour nettoyer un flux SSE.
En somme, une lecture « réussie » peut encore être erronée pour plusieurs raisons indépendantes : un drapeau de continuation peu fiable, une mauvaise interprétation de l’origine du comptage, un comportement des hooks spécifique au transport, ou encore une requête mise en œuvre sans protection.
Diriger chaque test vers la couche qu’il peut réellement vérifier
Aucune requête bout en bout ne peut valider toutes les couches en même temps.
Utilisez le parseur lorsque vous testez la validation du corps ou la structure de fusion forcée. Recourrez au délégué protégé lorsque la question concerne la projection en temps d’exécution ou les arguments de mutation finaux. Emploiez le chemin d’opération de l’extension pour vérifier si une injection automatique de contexte a réellement eu lieu.
Un outil indépendant de capture d’arguments vous permet d’examiner les arguments de mutation finaux sans toucher à une base de données, mais uniquement si vous reliez l’extension de protection à un délégué qui renvoie réellement les arguments qu’il a reçus. Créer un objet fictif non connecté ne prouve rien. Ce type d’outil vous indique quels arguments ont été émis, mais pas quelles lignes une base de données renverrait réellement.
Pour toute question concernant les résultats au niveau des locataires, la propriété des relations, le comportement transactionnel, les totaux distincts ou les particularités propres aux fournisseurs, vous avez besoin de fixtures soutenus par une base de données. Initialisez au moins deux locataires avec des enregistrements clairement différents les uns des autres afin que toute fuite de données soit immédiatement visible en cas d’occurrence.
Pour les questions relatives au routage généré, à la sérialisation, à l’exécution des hooks, à l’équivalence GET/POST, à la structure des réponses de pagination ou à la séquence des événements SSE, utilisez des tests au niveau HTTP.
Gardez au moins un test de contrat activant les mécanismes de protection, même si votre suite de tests bout en bout dans le navigateur fonctionne en mode désactivant cette validation. Un test de navigateur qui passe en mode assoupli ne prouve rien quant à ce que le environnement de production rejettera, car la couche de contrôle a été supprimée pour ce test.
Rédigez chaque test de régression au niveau le plus bas capable de prouver l’affirmation spécifique qu’il formule. Des tests plus ciblés signifient que, lorsque quelque chose tombe en panne par la suite, les points de défaillance se situent dans la phase responsable, sans avoir à reprendre l’ensemble du parcours de la requête depuis zéro.
Travaillez sur la défaillance dans une seule direction
Une séquence courte et reproductible vous évite de deviner les correctifs :
- Déterminez s’il s’agit d’une défaillance de démarrage, d’une défaillance en temps de requête ou d’une réponse réussie qui vous a surpris.
- Identifiez la phase responsable : le routeur, la résolution de l’appelant, la structure, la politique, l’exécution de Prisma ou le transport.
- Réduisez la reproduction à une seule opération, une seule structure et un seul corps de requête.
- Examinez l’argument au niveau le plus proche de l’origine du comportement.
Les API générées deviennent bien plus faciles à comprendre lorsque leurs différentes phases sont maintenues distinctes les unes des autres. Les erreurs de configuration doivent apparaître avant que tout trafic ne soit traité. Les requêtes qui enfreignent une règle doivent indiquer précisément quelle partie du contrat elles ont violée. De plus, une réponse réussie doit être vérifiée en fonction des arguments qu’elle a réellement émis ainsi que des spécifications de transport qui lui sont documentées, et non uniquement en fonction du code d’état.
Lectures complémentaires
- Corriger l’erreur de bibliothèque manquante libssl.so.1.1 dans Prisma sur Alpine Docker — Découvrez pourquoi le moteur de requêtes de Prisma plante sur les images Docker basées sur Alpine en raison d’une erreur de libssl manquante, et comment y remédier définitivement.
- Construire une API GraphQL sécurisée par le type avec Prisma et Nexus en Node.js — Suivez une démarche en sept étapes pour créer une API GraphQL Node.js qui unifie le modèle de données de Prisma avec les types et résolveurs générés par Nexus.