{"slug": "le-sdk-stripe-nous-a-menti-en-9-millisecondes-4-tests-pour-confondre-un-bug-d-de", "title": "Le SDK Stripe nous a menti en 9 millisecondes : 4 tests pour confondre un bug d'environnement avant de le patcher", "summary": "A developer at a company using Stripe encountered a StripeConnectionError that completed in only 9 milliseconds, indicating the SDK never made a network request. By running four targeted tests—reproducing in a preview environment, testing a minimal endpoint, bypassing the SDK with a direct fetch, and reading the source code at the error point—the developer isolated the bug to the SDK itself rather than the network or configuration. The incident was resolved by patching the environment, saving hours of debugging time.", "body_md": "Vendredi 15 mai, 16 h 13. L'alerte Sentry remonte sur le téléphone. La première réinscrite Phase 1 attend devant l'écran de paiement, son nom est en haut de mon onglet. Je pose la canette, je rouvre l'écran. La tasse à tête de Françoise, sur le poste d'à côté, capte un reflet jaune que je remarque sans le regarder. La stack trace tient en plein écran.\n\nLe stack trace s'ouvre, neuf champs sur dix à `null`\n\n, et un chiffre que je n'ai pas vu venir.\n\n```\ntype       = \"StripeConnectionError\"\nmessage    = \"An error occurred with our connection to Stripe.\"\ncode       = null\nstatusCode = null\nrequestId  = null\nduration   = 9 ms\n```\n\nNeuf millisecondes. Sur une route Vercel en région Paris, un DNS résout en quarante millisecondes, un *handshake* TLS coûte cent à deux cents. Neuf millisecondes, ce n'est pas un appel réseau qui a échoué. C'est un appel réseau qui n'a jamais eu lieu. Le SDK n'est pas arrivé jusqu'à la fibre.\n\nL'instinct propose immédiatement trois patchs. *Timeout serverless Vercel* — j'ajoute `maxDuration`\n\n, je redéploie. *Clé révoquée* — je vais la rouler. *Compte Stripe restreint après le passage en mode live* — j'ouvre un ticket support. Ces trois hypothèses sont plausibles. Aucune des trois n'est falsifiable par le symptôme seul, et c'est précisément ce qui les rend dangereuses : chacune ouvre un cycle de quinze à trente minutes avec rollback à la fin si elle se trompe. Multiplié par trois, on tient une demi-journée perdue avec la cliente toujours en train de cliquer.\n\nJe n'ai pas le temps. Une réinscrite attend.\n\nJe connais la classe d'incident — *« preview marche, prod casse »*, ou son symétrique. La règle, pour cette classe, c'est qu'on ne corrige rien tant qu'on n'a pas discriminé les couches. Quatre tests, exécutés dans l'ordre. Chacun élimine une famille d'hypothèses, pas une hypothèse isolée. Et chacun est conçu pour **réfuter** ce qu'il vient interroger — parce qu'un test qui cherche à confirmer trouve toujours, par sélection, ce qu'il cherche.\n\n**Test 1 — reproduire dans l'environnement témoin.** Je relance le même tunnel en preview, avec la clé `sk_test_`\n\n. Le Checkout s'ouvre en trois cent quatorze millisecondes, propre. Conséquence immédiate : ce n'est pas le code applicatif qui est en cause. Le code est strictement identique entre preview et prod ; seules varient les variables d'environnement, le plan Vercel sur cette région, et la clé Stripe. Trois variables seulement, et le brouillard se densifie déjà du bon côté.\n\n**Test 2 — endpoint minimal.** Je déploie une route Vercel d'une seule ligne utile, runtime nodejs forcé explicitement, qui appelle `stripe.balance.retrieve()`\n\n— le call SDK le plus dépouillé possible, sans `line_items`\n\n, sans `metadata`\n\n, sans `idempotencyKey`\n\n, sans rien de la complexité métier du Checkout. En preview : deux cents millisecondes, succès. En prod : neuf millisecondes, le même `StripeConnectionError`\n\n. Conséquence : le problème n'est pas dans les paramètres du Checkout. Il n'est pas non plus dans une logique métier qui aurait dérapé. Le SDK lui-même crashe au plus simple appel possible.\n\n**Test 3 — bypasser la dépendance suspecte.** Au lieu d'appeler le SDK, je `fetch`\n\ndirectement `https://api.stripe.com/v1/balance`\n\navec l'en-tête `Authorization: Bearer sk_live_…`\n\n. En prod, sur la même route Vercel : deux cents OK, trois cent quatorze millisecondes, payload qui confirme `livemode: true`\n\n. Conséquence — et c'est la conséquence la plus précieuse — l'infrastructure réseau Vercel→Stripe **fonctionne**. C'est strictement le SDK qui ne franchit pas la couche réseau. Ni Vercel, ni Cloudflare en amont, ni Stripe en aval ne sont en cause.\n\nNiran passe derrière l'épaule à ce moment-là, lit la sortie `curl`\n\nsur le terminal. Il prononce trois mots, *« c'est pas le réseau »*, et repart vers son poste sans relever davantage. Économie de gestes.\n\n**Test 4 — lire le source au point d'erreur exact.** Le stack trace m'indique `node_modules/stripe/esm/RequestSender.js:400:41`\n\n. J'ouvre le fichier dans le repo Vercel déployé. Ligne quatre cents, c'est le `.catch(error)`\n\nde la promise du HTTP client interne. Le SDK attendait une réponse de son propre client interne, et son propre client interne a rejeté immédiatement, avant même d'émettre une requête. Je remonte dans le `package.json`\n\nde la lib :\n\n```\n\"exports\": {\n  \"worker\": {\n    \"import\": \"./esm/stripe.esm.worker.js\",\n    \"require\": \"./cjs/stripe.cjs.worker.js\"\n  },\n  \"default\": {\n    \"import\": {\n      \"default\": \"./esm/stripe.esm.node.js\"\n    }\n  }\n}\n```\n\nVoilà ce qui se passait. Le `package.json`\n\nde `stripe^22`\n\ndéclare un export conditionnel `\"worker\"`\n\ndestiné aux environnements Cloudflare Workers. Le bundler Next 16, malgré `export const runtime = 'nodejs'`\n\nexplicitement déclaré au sommet de la route, résout cette condition `\"worker\"`\n\nau moment du bundle des Server Actions en production. Le bundle charge alors `stripe.esm.worker.js`\n\n, une variante du SDK qui repose sur le `fetch`\n\nstandard du runtime Worker et qui n'a pas le HTTP client Node natif. Cette variante, exécutée sur le runtime Node de Vercel, échoue silencieusement à l'initialisation de son HTTP client — pour une raison probablement liée à une feature Cloudflare absente du runtime Vercel — et la promise du tout premier *request* se rejette dans la milliseconde qui suit.\n\nL'hypothèse n'est pas confirmée à cent pour cent. Mais elle est cohérente avec les trois faits matériels accumulés : l'écart prod/preview qui dépend du contexte de bundle, l'échec en neuf millisecondes synchrone sans réseau, l'absence totale de `requestId`\n\nparce qu'aucune requête n'a jamais été émise.\n\nEn vingt minutes, le diagnostic tient. En quarante minutes de plus, le helper `lib/stripe-fetch.ts`\n\nest en production sur six surfaces — Checkout Sessions, retrieve PaymentIntent, retrieve BalanceTransaction, create off_session PaymentIntent, retrieve Checkout Session, et Payment Links de facturation.\n\n```\n// lib/stripe-fetch.ts\nexport async function stripePost<T = unknown>(\n  path: string,\n  params: Record<string, ParamValue>,\n  options?: { idempotencyKey?: string },\n): Promise<T> {\n  const headers: Record<string, string> = {\n    Authorization: `Bearer ${getKey()}`,\n    'Content-Type': 'application/x-www-form-urlencoded',\n  }\n  if (options?.idempotencyKey) headers['Idempotency-Key'] = options.idempotencyKey\n  const res = await fetch(`https://api.stripe.com/v1/${path}`, {\n    method: 'POST',\n    headers,\n    body: encodeParams(params),\n  })\n  return parseStripeResponse<T>(res)\n}\n\n// app/inscription/actions.ts::finaliserReinscription (excerpt)\nconst stripeRes = await fetch('https://api.stripe.com/v1/checkout/sessions', {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${stripeKey}`, 'Content-Type': 'application/x-www-form-urlencoded' },\n  body: encodeParams(checkoutParams),\n})\n```\n\nÀ 17h35, je relance le tunnel en prod avec une fausse fiche : la session Checkout s'ouvre, *livemode* confirmé, méthodes carte plus Link plus Google Pay. La cliente de 16h13 reçoit l'email d'excuse et le nouveau lien dans la foulée. Phase 2 du lundi 19/05, soixante-cinq anciens à relancer, débloquée matériellement.\n\nSi j'avais commencé par patcher le timeout, j'aurais redéployé, attendu cinq minutes, retesté, constaté l'échec, retiré le patch, attendu encore cinq minutes : un cycle d'environ vingt minutes. À ajouter à la roulette de la clé — quinze minutes le temps de générer, propager, attendre l'invalidation des caches Vercel. Et au ticket support Stripe : entre deux et quarante-huit heures, opaques, pendant que la production saigne. Comparé à ces trois patchs, le protocole tient en moins de trente minutes et débouche sur la vraie cause — pas sur un voisin de la vraie cause.\n\nLe protocole vaut pour toute classe « le même code se comporte différemment selon l'environnement ». Les symptômes-déclencheurs que je remets désormais en tête de file : `StripeConnectionError`\n\n, `ECONNREFUSED`\n\nou `ETIMEDOUT`\n\nau runtime mais pas au build, `Module not found`\n\nqui n'apparaît qu'en prod, ou pire encore — un `try / catch`\n\nsilencieux qui retourne un fallback trompeur et fait croire que la branche principale a réussi. Quatre tests, dans le même ordre. Témoin, minimal, bypass, source.\n\nLe protocole ne vaut **pas** pour les bugs métier — une `query`\n\nSQL fausse, un `if`\n\nmal calibré, une logique applicative qui rend le mauvais résultat. Là, la cause est dans le code que vous avez écrit, et c'est un grep ciblé qui la trouve, pas une discrimination de couches.\n\nOn ne corrige pas un défaut de cuisson en regardant la pièce. On regarde la courbe du four, le poste de gaz, le tirage de la cheminée. Le code applicatif, c'est la pièce — il sort tel qu'on l'a façonné. Les quatre tests interrogent le four. Chacun éteint une lampe possible jusqu'à ce qu'il n'en reste qu'une, qui est la bonne. Trente minutes au lieu d'une demi-journée, et surtout : la certitude d'avoir patché *là où il fallait*, pas dans un voisinage flatteur qui laisse le vrai bug dormir jusqu'au prochain incident.\n\nLe protocole 4 tests est l'instance applicative de la règle R4 *Falsify before fix* du Counterpart Toolkit, sur la classe d'incident « bug d'environnement ». La règle générale demande trois sondes conçues pour réfuter ; cette classe-ci en mérite quatre, dans un ordre figé. C'est tout. Mais ce *tout*, le jour où la production saigne, vaut la demi-journée qu'il vous fait gagner.\n\n*Counterpart Toolkit v0.7, R4 *Falsify before fix*. Référence canonique : github.com/michelfaure/doctrine-counterpart. Scènes recomposées, prénoms calibrés sur les fiches cast récurrentes de la série.*", "url": "https://wpnews.pro/news/le-sdk-stripe-nous-a-menti-en-9-millisecondes-4-tests-pour-confondre-un-bug-d-de", "canonical_source": "https://dev.to/michelfaure/le-sdk-stripe-nous-a-menti-en-9-millisecondes-4-tests-pour-confondre-un-bug-denvironnement-avant-40ic", "published_at": "2026-06-14 09:56:27+00:00", "updated_at": "2026-06-14 10:10:35.374700+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["Stripe", "Vercel", "Sentry", "Niran"], "alternates": {"html": "https://wpnews.pro/news/le-sdk-stripe-nous-a-menti-en-9-millisecondes-4-tests-pour-confondre-un-bug-d-de", "markdown": "https://wpnews.pro/news/le-sdk-stripe-nous-a-menti-en-9-millisecondes-4-tests-pour-confondre-un-bug-d-de.md", "text": "https://wpnews.pro/news/le-sdk-stripe-nous-a-menti-en-9-millisecondes-4-tests-pour-confondre-un-bug-d-de.txt", "jsonld": "https://wpnews.pro/news/le-sdk-stripe-nous-a-menti-en-9-millisecondes-4-tests-pour-confondre-un-bug-d-de.jsonld"}}