{"record":{"id":"211f9090c96e619f","repo":"Stirling-Tools/Stirling-PDF","slug":"quote-pdf-response-was-not-a-file","errorCode":null,"errorMessage":"quote PDF response was not a file","messagePattern":"quote PDF response was not a file","errorType":"exception","errorClass":"StripeFunctionError","httpStatus":null,"severity":"error","filePath":"frontend/editor/src/portal/billing/stripe.ts","lineNumber":481,"sourceCode":" */\nexport async function fetchBundleQuotePdf(quoteId: number): Promise<Blob> {\n  ensureSaasSupabase();\n  const supabase = getSupabaseClient();\n  if (!supabase) {\n    throw new StripeFunctionError(\n      \"SaaS Supabase not configured — set VITE_SUPABASE_URL.\",\n      \"unconfigured\",\n    );\n  }\n  const { data, error } = await supabase.functions.invoke<Blob>(\n    `create-payg-bundle-quote?quote_id=${quoteId}`,\n    { method: \"GET\" },\n  );\n  if (error) {\n    throw new StripeFunctionError(error.message ?? \"quote PDF fetch failed\");\n  }\n  if (!(data instanceof Blob)) {\n    throw new StripeFunctionError(\"quote PDF response was not a file\");\n  }\n  return data;\n}\n\n/**\n * {@code VITE_STRIPE_PUBLISHABLE_KEY} — the Stripe pk used by embedded Checkout. Coalesces to \"\" when\n * unset so the declared `string` return type is honest (Vite substitutes `undefined` for a missing\n * env var); callers guard with a falsy check.\n */\nexport function getStripePublishableKey(): string {\n  return import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY ?? \"\";\n}\n\n// Process-wide memoized Stripe.js loader, shared by the embedded-checkout modals so the SDK promise\n// is created once rather than per-modal. loadStripe is dynamically imported so its chunk only loads\n// when a checkout modal reaches its payment step.\nlet stripePromise: Promise<Stripe | null> | null = null;\nexport function loadStripeOnce(pk: string): Promise<Stripe | null> {","sourceCodeStart":463,"sourceCodeEnd":499,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/portal/billing/stripe.ts#L463-L499","documentation":"Thrown by fetchBundleQuotePdf when the GET invocation resolves without error but the returned data is not a Blob. The route is expected to stream application/pdf; any other body shape (JSON error object, text, undefined) fails the instanceof Blob check.","triggerScenarios":"create-payg-bundle-quote GET returns successfully but the body is not a Blob — e.g. the function returned a JSON error object instead of streaming the PDF, or supabase-js parsed the response into a plain object because the content-type wasn't application/pdf.","commonSituations":"The function returned an error as JSON (e.g. { error: 'not found' }) with status 200; the route forgot to set Content-Type: application/pdf; a proxy re-wrapped the body; supabase-js version changed Blob handling for non-binary content-types.","solutions":["Confirm the GET route always streams the PDF with Content-Type: application/pdf on success.","On the function's error paths, return a proper HTTP error status (not 200 + JSON) so the invoke error branch (157) fires instead.","Check the supabase-js version — Blob parsing depends on the response content-type.","Log the actual data type/constructor in dev to identify what was returned."],"exampleFix":"// edge function (GET route) — before\nif (!pdf) return json({ error: 'not_found' }, { status: 200 });\n\n// after — error paths use a non-2xx status so invoke rejects\nif (!pdf) return json({ error: 'not_found' }, { status: 404 });\nreturn new Response(pdf, { headers: { 'Content-Type': 'application/pdf' } });","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isPdfBlob(data: unknown): data is Blob {\n  return typeof Blob !== \"undefined\" && data instanceof Blob && data.size > 0;\n}","tryCatchPattern":"try {\n  const data = await fetchBundleQuotePdf(quoteId); // throws if not a Blob\n  downloadBlob(data, `quote-${quoteId}.pdf`);\n} catch (e) {\n  if (e instanceof StripeFunctionError && /not a file/i.test(e.message)) {\n    notifications.show({ message: \"Quote PDF came back in the wrong format.\", color: \"red\" });\n  } else throw e;\n}","preventionTips":["Ensure the GET route streams the PDF with Content-Type: application/pdf on success.","Return a non-2xx status (not 200 + JSON) on error paths so invoke rejects rather than returning a non-Blob.","Verify the supabase-js version handles application/pdf as a Blob."],"tags":["billing","stripe","supabase","edge-function","pdf","contract","content-type"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}