ruvnet/ruflo · error · Error
Failed to fetch ${url}: ${response.status} ${response.status
Error message
Failed to fetch ${url}: ${response.status} ${response.statusText} What it means
Generic non-2xx guard in the shared fetchJSON<T> helper used across the client. It calls (options?.fetch ?? fetch)(url) and throws when response.ok is false, embedding the URL, status code, and statusText. It does not read the body, so the error never contains the server's JSON error detail.
Source
Thrown at ruflo/src/ruvocal/src/lib/utils/fetchJSON.ts:10
export async function fetchJSON<T>(
url: string,
options?: {
fetch?: typeof window.fetch;
allowNull?: boolean;
}
): Promise<T> {
const response = await (options?.fetch ?? fetch)(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
}
// Handle empty responses (which parse to null)
const text = await response.text();
if (!text || text.trim() === "") {
if (options?.allowNull) {
return null as T;
}
throw new Error(`Received empty response from ${url} but allowNull is not set to true`);
}
return JSON.parse(text);
}
View on GitHub (pinned to 6b01dc5a68)
Solutions
- Read response.status from the error text to classify (4xx = caller fault, 5xx = server).
- For 401/403, ensure credentials/tokens are attached to the request (fetchJSON does not add auth headers itself).
- For 404, confirm the URL and the API version path.
- For 429/5xx, wrap the call in a retry with exponential backoff.
- If you need the body, switch to a custom fetch wrapper that reads response.text() before throwing.
Example fix
// before
const response = await (options?.fetch ?? fetch)(url);
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
// after
const response = await (options?.fetch ?? fetch)(url);
if (!response.ok) {
const detail = await response.text().catch(() => "");
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText} ${detail.slice(0, 200)}`);
} Defensive patterns
Strategy: retry
Validate before calling
async function canFetch(url: string): Promise<boolean> {
try {
const r = await fetch(url, { method: "HEAD" });
return r.ok || r.status === 405; // some endpoints disallow HEAD
} catch {
return false;
}
} Try / catch
async function fetchJSONRetry<T>(url: string, attempts = 3): Promise<T> {
let lastErr: unknown;
for (let i = 0; i < attempts; i++) {
try {
return await fetchJSON<T>(url);
} catch (e) {
lastErr = e;
const status = Number(String((e as Error)?.message).match(/:\s*(\d{3})/)?.[1] ?? 0);
if (status >= 400 && status < 500 && status !== 429) throw e; // do not retry caller faults
await new Promise((r) => setTimeout(r, 2 ** i * 200));
}
}
throw lastErr;
} Prevention
- Attach auth headers/credentials at the call site — fetchJSON does not do it for you.
- Classify by status: 4xx (except 429) = fail fast, 5xx/429/network = retry with backoff.
- Log the URL + status so triage can distinguish auth (401) from upstream (502).
When it happens
Trigger: Any fetchJSON call against an endpoint that returns 4xx/5xx: unauthenticated (401), forbidden (403), not found (404), upstream error (502/503), or a rate-limit (429).
Common situations: Forgetting to send an auth token; hitting a route that only exists in v2 while calling v1; the upstream model/API being down; CORS preflight rejected and surfaced as an opaque non-OK response.
Related errors
- Received empty response from ${url} but allowNull is not set
- Failed to create share link
- MCP server "${server.name}" is in cooldown (HTTP ${cd.status
- MCP server "${server.name}" returned HTTP ${httpStatus}: ${h
- Failed to fetch ${baseURL}/models: ${response.status} ${resp
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/52f613e7b98843b9.
Report an issue: GitHub.