{"record":{"id":"c0f5d6a4df024103","repo":"Stirling-Tools/Stirling-PDF","slug":"edge-function-returned-no-client-secret-c0f5d6","errorCode":null,"errorMessage":"Edge function returned no client_secret","messagePattern":"Edge function returned no client_secret","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"frontend/editor/src/cloud/components/shared/config/configSections/StripeCheckoutPanel.tsx","lineNumber":199,"sourceCode":"        // return URL (browser origin on web, deep link on desktop).\n        const session = await createCheckoutSession({\n          teamId,\n          currency,\n          // Maps to Stripe's customer_email when the team has no Stripe\n          // customer yet — prefills + locks the email field in Checkout. Teams\n          // with an existing customer get the email locked from the customer\n          // record instead; this field is ignored for them.\n          billingOwnerEmail: billingEmail,\n        });\n        if (cancelled) return;\n        // Hosted-url fallback: no embedded iframe, hand the URL to the system\n        // browser. The deep-link / origin return URL brings the user back.\n        if (session.url && !session.clientSecret) {\n          await openExternal(session.url);\n          return;\n        }\n        if (!session.clientSecret) {\n          throw new Error(\"Edge function returned no client_secret\");\n        }\n        setClientSecret(session.clientSecret);\n        setIsMock(\n          Boolean(session.mock) || session.clientSecret.startsWith(\"cs_mock_\"),\n        );\n      } catch (e: unknown) {\n        if (cancelled) return;\n        const msg =\n          e instanceof Error\n            ? e.message\n            : tRef.current(\n                \"payg.checkout.error.startFailed\",\n                \"Couldn't start checkout session\",\n              );\n        setError(msg);\n        onErrorRef.current?.(msg);\n      } finally {\n        if (!cancelled) setLoading(false);","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/cloud/components/shared/config/configSections/StripeCheckoutPanel.tsx#L181-L217","documentation":"Thrown in StripeCheckoutPanel.tsx as a component-level defensive guard after createCheckoutSession() returns. If the session object has no url (hosted fallback) and no clientSecret, the checkout cannot proceed. This is typically unreachable because billing.ts createCheckoutSession() already throws this same error — but if the billing seam is shadowed by a platform override (desktop/cloud) that returns an unexpected shape, this guard catches it. The error is caught by the component's own try/catch and surfaced as setError(msg).","triggerScenarios":"A platform-specific override of createCheckoutSession (via @app/* shadow in desktop or cloud layer) returns an object with neither url nor clientSecret. Or the billing seam's return type is widened/changed and the component receives an empty object.","commonSituations":"Desktop or cloud build shadows createCheckoutSession with a stub that returns {}; billing service contract changed but the platform override wasn't updated; the edge function returns a response that the seam maps to an empty CheckoutSession.","solutions":["If using a platform override of createCheckoutSession, ensure it always returns either { clientSecret, mock } or { url }","Verify the @app/* billing seam resolves to the correct implementation for the current build flavor","Check the create-checkout-session edge function logs if the real billing.ts implementation is in use","Ensure STRIPE_SECRET_KEY is configured in the edge function"],"exampleFix":"// before\nif (!session.clientSecret) {\n  throw new Error(\"Edge function returned no client_secret\");\n}\n\n// after\nif (!session.clientSecret) {\n  throw new Error(\n    session.url\n      ? \"Checkout returned only a URL but the hosted path was already handled\"\n      : \"Edge function returned no client_secret\",\n  );\n}","handlingStrategy":"try-catch","validationCode":"// Verify teamId is valid before checkout\nif (!teamId) {\n  setError('A team is required to start checkout');\n  return;\n}","typeGuard":"function isUsableSession(session: CheckoutSession | null): session is { clientSecret: string; mock?: boolean } {\n  return session !== null && typeof session.clientSecret === 'string' && session.clientSecret.length > 0;\n}","tryCatchPattern":"// The component already catches at line 205:\n// catch (e: unknown) {\n//   if (cancelled) return;\n//   const msg = e instanceof Error ? e.message : t('payg.checkout.error.startFailed');\n//   setError(msg);\n//   onErrorRef.current?.(msg);\n// }","preventionTips":["If shadowing createCheckoutSession in a platform layer, ensure it always returns clientSecret or url","Verify the @app/* billing seam resolves to the correct implementation for the current build flavor","Check STRIPE_SECRET_KEY and edge function configuration if using the real billing.ts implementation","Test the checkout flow in each build flavor (core, saas, desktop) to catch seam resolution issues"],"tags":["billing","stripe","checkout","component","saas","defensive-guard"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}