Un nombre qui mentait dans chaque réponse
Prisma Decimal serialised as {"s":1,"e":4,"d":[15000]} because a global NestJS response interceptor rebuilt objects with Object.entries and stripped the prototype.
Ça disait que ça marchait
Vert, 200, enregistré, « déjà géré » — et faux. Aucune erreur sur laquelle s'appuyer.
Je corrigeais l'endpoint des frais de programme qui rejetait chaque champ, ce qui s'est révélé être un import type sur le DTO qui retirait les métadonnées d'exécution dont la validation en liste blanche de NestJS a besoin. J'ai corrigé ça, puis j'ai réellement lu le corps de la réponse au lieu de vérifier le code de statut, et le montant des frais n'était pas un nombre — c'était {"s":1,"e":4,"d":[15000]}, la représentation interne de decimal.js qui fuyait à travers.
L'intercepteur de réponse global existe pour convertir en chaîne les BigInt, que JSON ne peut pas sérialiser. Pour le faire de manière générique, il reconstruit toute valeur en forme d'objet via Object.entries(), ce qui arrache le prototype de tout ce qui a la forme d'un objet et passe par là — y compris le Decimal de Prisma. Decimal a un toJSON()/toNumber() qui marche, mais seulement tant que son prototype survit. Donc chaque Decimal dans chaque réponse était faux : frais, paiements, échéances.
Rien ne levait d'exception, chaque endpoint continuait de renvoyer 200. Invisible à moins de lire un vrai corps de réponse — un typecheck ne peut pas l'attraper, un test « est-ce que la requête réussit » ne peut pas l'attraper, et lire le code appelant non plus, parce que la corruption arrive après le handler, dans un middleware partagé que personne ne re-vérifie une fois qu'il est fait.
Corrigé avec une branche instanceof Prisma.Decimal explicite avant le parcours générique. Du code générique qui démonte et reconstruit des objets arbitraires pour résoudre un problème étroit finira par rencontrer un type pour lequel il n'a pas été écrit, et quand il occupe une position globale, le rayon de souffle est toute l'API d'un coup.