{"record":{"id":"52f613e7b98843b9","repo":"ruvnet/ruflo","slug":"failed-to-fetch-url-response-status-respo","errorCode":null,"errorMessage":"Failed to fetch ${url}: ${response.status} ${response.statusText}","messagePattern":"Failed to fetch (.+?): (.+?) (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/ruvocal/src/lib/utils/fetchJSON.ts","lineNumber":10,"sourceCode":"export async function fetchJSON<T>(\n\turl: string,\n\toptions?: {\n\t\tfetch?: typeof window.fetch;\n\t\tallowNull?: boolean;\n\t}\n): Promise<T> {\n\tconst response = await (options?.fetch ?? fetch)(url);\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);\n\t}\n\n\t// Handle empty responses (which parse to null)\n\tconst text = await response.text();\n\tif (!text || text.trim() === \"\") {\n\t\tif (options?.allowNull) {\n\t\t\treturn null as T;\n\t\t}\n\t\tthrow new Error(`Received empty response from ${url} but allowNull is not set to true`);\n\t}\n\n\treturn JSON.parse(text);\n}\n","sourceCodeStart":1,"sourceCodeEnd":24,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/ruvocal/src/lib/utils/fetchJSON.ts#L1-L24","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nconst response = await (options?.fetch ?? fetch)(url);\nif (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);\n// after\nconst response = await (options?.fetch ?? fetch)(url);\nif (!response.ok) {\n  const detail = await response.text().catch(() => \"\");\n  throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText} ${detail.slice(0, 200)}`);\n}","handlingStrategy":"retry","validationCode":"async function canFetch(url: string): Promise<boolean> {\n  try {\n    const r = await fetch(url, { method: \"HEAD\" });\n    return r.ok || r.status === 405; // some endpoints disallow HEAD\n  } catch {\n    return false;\n  }\n}","typeGuard":null,"tryCatchPattern":"async function fetchJSONRetry<T>(url: string, attempts = 3): Promise<T> {\n  let lastErr: unknown;\n  for (let i = 0; i < attempts; i++) {\n    try {\n      return await fetchJSON<T>(url);\n    } catch (e) {\n      lastErr = e;\n      const status = Number(String((e as Error)?.message).match(/:\\s*(\\d{3})/)?.[1] ?? 0);\n      if (status >= 400 && status < 500 && status !== 429) throw e; // do not retry caller faults\n      await new Promise((r) => setTimeout(r, 2 ** i * 200));\n    }\n  }\n  throw lastErr;\n}","preventionTips":["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)."],"tags":["network","fetch","http","utility"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}