Stirling-Tools/Stirling-PDF · error · Error
SaaS request failed (${res.status})
Error message
SaaS request failed (${res.status}) What it means
Generic Error thrown by saasText() when the fetch returned a non-ok status. Unlike the JSON paths (which use HttpError with status+statusText+body), this throws a plain Error with only `SaaS request failed (${res.status})` — no statusText, no body, and it is NOT an HttpError instance. This is an inconsistency: callers using `instanceof HttpError` will miss it.
Source
Thrown at frontend/editor/src/portal/api/http.ts:286
path: string,
options: HttpRequestOptions = {},
): Promise<string> {
const base = saasBaseUrl();
// null = unset (self-hosted, no VITE_SAAS_API_URL). "" is same-origin (SaaS) — valid.
if (base === null) throw new SaasUnconfiguredError();
const token = await getPortalSaasToken();
if (!token) throw new SaasNotLinkedError();
const res = await fetch(`${base}${path}`, {
method: options.method ?? "GET",
headers: {
Accept: "text/plain",
Authorization: `Bearer ${token}`,
...options.headers,
},
signal: options.signal,
});
if (!res.ok) {
throw new Error(`SaaS request failed (${res.status})`);
}
return res.text();
}
/** SaaS GET returning a binary Blob, with the Supabase JWT attached. */
async function saasBlob(
path: string,
options: HttpRequestOptions = {},
): Promise<Blob> {
const base = saasBaseUrl();
// Same-origin SaaS resolves to "" (falsy); only null means unconfigured.
if (base === null) throw new SaasUnconfiguredError();
const token = await getPortalSaasToken();
if (!token) throw new SaasNotLinkedError();
const res = await fetch(`${base}${path}`, {
method: options.method ?? "GET",
headers: { Authorization: `Bearer ${token}`, ...options.headers },
signal: options.signal,View on GitHub (pinned to 9ef20dcab8)
Solutions
- Catch broadly (Error) for saasText results since it is not an HttpError — or check the message prefix 'SaaS request failed'.
- Fix the inconsistency at the source: throw new HttpError(res.status, res.statusText, await res.text().catch(() => null)) so callers get a uniform error type with the body.
- For 404, verify the resource id/path; for 401/403 refresh the portalSaasSession token.
Example fix
// before — plain Error, loses statusText + body, breaks instanceof HttpError
if (!res.ok) {
throw new Error(`SaaS request failed (${res.status})`);
}
// after — uniform HttpError so callers handle text + json paths identically
if (!res.ok) {
const detail = await res.text().catch(() => null);
throw new HttpError(res.status, res.statusText, detail);
} Defensive patterns
Strategy: try-catch
Validate before calling
null
Type guard
// NOT an HttpError — must match by message. (Fix the source to throw HttpError instead.)
function isSaasTextFailure(e: unknown): boolean {
return e instanceof Error && /^SaaS request failed \(\d+\)$/.test(e.message);
}
function saasTextStatus(e: unknown): number | null {
if (!isSaasTextFailure(e)) return null;
const m = (e as Error).message.match(/\((\d+)\)/);
return m ? Number(m[1]) : null;
} Try / catch
try { return await apiClient.saas.text(path); }
catch (e) {
if (isSaasTextFailure(e)) {
const status = saasTextStatus(e);
if (status === 404) return null;
}
throw e;
} Prevention
- saasText throws a plain Error, not HttpError — do not rely on instanceof HttpError here.
- Fix the inconsistency: change the source to throw new HttpError(res.status, res.statusText, body).
- Parse the status from the message until the source is unified.
When it happens
Trigger: saasText fetch completes but res.ok is false — a 4xx/5xx from the SaaS backend on a text endpoint (e.g. licence file not found, server error).
Common situations: The licence/resource requested does not exist (404); the SaaS backend errored generating the text (500); a stale JWT produced 401/403 on the text endpoint.
Related errors
- ${status} ${statusText}
- SaaS API not configured — set VITE_SAAS_API_URL to enable po
- No SaaS session — admin must link an account before attended
- No SaaS session
- No team to invite to
AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13).
Data as JSON: /api/errors/ce7eb9c6b5af7470.
Report an issue: GitHub.