Le SDK Stripe nous a menti en 9 millisecondes : 4 tests pour confondre un bug d'environnement avant de le patcher 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. 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. Le stack trace s'ouvre, neuf champs sur dix à null , et un chiffre que je n'ai pas vu venir. type = "StripeConnectionError" message = "An error occurred with our connection to Stripe." code = null statusCode = null requestId = null duration = 9 ms Neuf 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. L'instinct propose immédiatement trois patchs. Timeout serverless Vercel — j'ajoute maxDuration , 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. Je n'ai pas le temps. Une réinscrite attend. Je 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. Test 1 — reproduire dans l'environnement témoin. Je relance le même tunnel en preview, avec la clé sk test . 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é. Test 2 — endpoint minimal. Je déploie une route Vercel d'une seule ligne utile, runtime nodejs forcé explicitement, qui appelle stripe.balance.retrieve — le call SDK le plus dépouillé possible, sans line items , sans metadata , sans idempotencyKey , sans rien de la complexité métier du Checkout. En preview : deux cents millisecondes, succès. En prod : neuf millisecondes, le même StripeConnectionError . 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. Test 3 — bypasser la dépendance suspecte. Au lieu d'appeler le SDK, je fetch directement https://api.stripe.com/v1/balance avec l'en-tête Authorization: Bearer sk live … . En prod, sur la même route Vercel : deux cents OK, trois cent quatorze millisecondes, payload qui confirme livemode: true . 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. Niran passe derrière l'épaule à ce moment-là, lit la sortie curl sur le terminal. Il prononce trois mots, « c'est pas le réseau » , et repart vers son poste sans relever davantage. Économie de gestes. Test 4 — lire le source au point d'erreur exact. Le stack trace m'indique node modules/stripe/esm/RequestSender.js:400:41 . J'ouvre le fichier dans le repo Vercel déployé. Ligne quatre cents, c'est le .catch error de 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 de la lib : "exports": { "worker": { "import": "./esm/stripe.esm.worker.js", "require": "./cjs/stripe.cjs.worker.js" }, "default": { "import": { "default": "./esm/stripe.esm.node.js" } } } Voilà ce qui se passait. Le package.json de stripe^22 déclare un export conditionnel "worker" destiné aux environnements Cloudflare Workers. Le bundler Next 16, malgré export const runtime = 'nodejs' explicitement déclaré au sommet de la route, résout cette condition "worker" au moment du bundle des Server Actions en production. Le bundle charge alors stripe.esm.worker.js , une variante du SDK qui repose sur le fetch standard 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. L'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 parce qu'aucune requête n'a jamais été émise. En vingt minutes, le diagnostic tient. En quarante minutes de plus, le helper lib/stripe-fetch.ts est en production sur six surfaces — Checkout Sessions, retrieve PaymentIntent, retrieve BalanceTransaction, create off session PaymentIntent, retrieve Checkout Session, et Payment Links de facturation. // lib/stripe-fetch.ts export async function stripePost