Stirling-Tools/Stirling-PDF · error · StripeFunctionError

quote PDF fetch failed

Error message

quote PDF fetch failed

What it means

Fallback message in fetchBundleQuotePdf when the GET invocation of create-payg-bundle-quote returns an error whose .message is falsy. The PDF fetch failed at the edge function, but the error carried no human-readable text, so the helper substitutes a generic description.

Source

Thrown at frontend/editor/src/portal/billing/stripe.ts:478

/**
 * Fetch the Stripe-rendered quote PDF for a persisted quote, via the {@code create-payg-bundle-quote}
 * GET route (streams application/pdf). Returns a Blob the caller can object-URL for download.
 */
export async function fetchBundleQuotePdf(quoteId: number): Promise<Blob> {
  ensureSaasSupabase();
  const supabase = getSupabaseClient();
  if (!supabase) {
    throw new StripeFunctionError(
      "SaaS Supabase not configured — set VITE_SUPABASE_URL.",
      "unconfigured",
    );
  }
  const { data, error } = await supabase.functions.invoke<Blob>(
    `create-payg-bundle-quote?quote_id=${quoteId}`,
    { method: "GET" },
  );
  if (error) {
    throw new StripeFunctionError(error.message ?? "quote PDF fetch failed");
  }
  if (!(data instanceof Blob)) {
    throw new StripeFunctionError("quote PDF response was not a file");
  }
  return data;
}

/**
 * {@code VITE_STRIPE_PUBLISHABLE_KEY} — the Stripe pk used by embedded Checkout. Coalesces to "" when
 * unset so the declared `string` return type is honest (Vite substitutes `undefined` for a missing
 * env var); callers guard with a falsy check.
 */
export function getStripePublishableKey(): string {
  return import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY ?? "";
}

// Process-wide memoized Stripe.js loader, shared by the embedded-checkout modals so the SDK promise
// is created once rather than per-modal. loadStripe is dynamically imported so its chunk only loads

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Inspect the full StripeFunctionError / underlying error in dev tools for a code or cause, even when message is empty.
  2. Check the create-payg-bundle-quote GET route logs in the Supabase dashboard.
  3. Verify quoteId is valid and the GET route accepts ?quote_id=.
  4. Confirm CORS and content-type handling on the GET route (it streams application/pdf).

Example fix

// before
throw new StripeFunctionError(error.message ?? "quote PDF fetch failed");

// after — expose more of the underlying error
const e = error as { code?: string; hint?: string };
throw new StripeFunctionError(
  error.message ?? `quote PDF fetch failed (code=${e.code ?? "?"})`,
  e.code,
);
Defensive patterns

Strategy: try-catch

Type guard

function isFunctionsError(e: unknown): e is { message?: string; code?: string } {
  return typeof e === "object" && e !== null && "message" in e;
}

Try / catch

try {
  const pdf = await fetchBundleQuotePdf(quoteId);
  downloadBlob(pdf);
} catch (e) {
  if (e instanceof StripeFunctionError) {
    notifications.show({ message: `Could not download quote PDF: ${e.message}`, color: "red" });
  }
}

Prevention

When it happens

Trigger: supabase.functions.invoke against the create-payg-bundle-quote GET route rejects with an error object lacking message (or with message === ''). This is the right-hand operand of `error.message ?? fallback`.

Common situations: Transport/CORS error on the GET route; edge function threw a non-Error value; the function returned an error status with an empty body; supabase-js version changed the error shape.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/9d08b03f3f6a5f35. Report an issue: GitHub.