{"record":{"id":"f99c32a593f39d4a","repo":"jackwener/OpenCLI","slug":"fetch-error-f99c32","errorCode":"FETCH_ERROR","errorMessage":"HTTP ${resp.status} ${resp.statusText} from ${finalUrl}","messagePattern":"HTTP (.+?) (.+?) from (.+?)","errorType":"error_code","errorClass":"CliError","httpStatus":null,"severity":"error","filePath":"src/pipeline/steps/fetch.ts","lineNumber":34,"sourceCode":"  page: IPage | null, url: string, method: string,\n  queryParams: Record<string, unknown>, headers: Record<string, unknown>,\n  args: Record<string, unknown>, data: unknown,\n): Promise<unknown> {\n  const renderedParams: Record<string, string> = {};\n  for (const [k, v] of Object.entries(queryParams)) renderedParams[k] = String(render(v, { args, data }));\n  const renderedHeaders: Record<string, string> = {};\n  for (const [k, v] of Object.entries(headers)) renderedHeaders[k] = String(render(v, { args, data }));\n\n  let finalUrl = url;\n  if (Object.keys(renderedParams).length > 0) {\n    const qs = new URLSearchParams(renderedParams).toString();\n    finalUrl = `${finalUrl}${finalUrl.includes('?') ? '&' : '?'}${qs}`;\n  }\n\n  if (page === null) {\n    const resp = await fetch(finalUrl, { method: method.toUpperCase(), headers: renderedHeaders });\n    if (!resp.ok) {\n      throw new CliError('FETCH_ERROR', `HTTP ${resp.status} ${resp.statusText} from ${finalUrl}`);\n    }\n    return resp.json();\n  }\n\n  return page.fetchJson(finalUrl, { method: method.toUpperCase(), headers: renderedHeaders });\n}\n\n/**\n * Batch fetch: send all URLs into the browser as a single evaluate() call.\n * This eliminates N-1 cross-process IPC round trips, performing all fetches\n * inside the V8 engine and returning results as one JSON array.\n */\nasync function fetchBatchInBrowser(\n  page: IPage, urls: string[], method: string,\n  headers: Record<string, string>, concurrency: number,\n): Promise<unknown[]> {\n  const headersJs = JSON.stringify(headers);\n  const urlsJs = JSON.stringify(urls);","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/src/pipeline/steps/fetch.ts#L16-L52","documentation":"fetchSingle performs an HTTP request via fetch() and throws a CliError with code FETCH_ERROR when the response is not ok (non-2xx status). The message includes the HTTP status, statusText, and the fully rendered target URL so the developer can see exactly which request failed and why. It exists to surface remote HTTP failures as a typed, recognizable pipeline error instead of continuing with invalid data.","triggerScenarios":"Any fetch step whose rendered URL/headers produce a non-ok response: 404 for a wrong path, 401/403 for missing or invalid renderedHeaders auth, 500 from the upstream server, 429 rate limiting. Thrown only on the page===null (non-paginated) branch; paginated requests go through page.fetchJson instead.","commonSituations":"Expired API tokens baked into headers, typos in the configured URL or query params after template rendering, the upstream API being down or rate-limiting bulk pipeline runs, an endpoint renamed after an API version change.","solutions":["Open the URL in the message with curl (with the same headers) to confirm the status and inspect the response body","Fix the URL or path in the step config so it points at an existing endpoint","Check/refresh the credentials in renderedHeaders for 401/403, and add rate-limit backoff for 429","Add retry/backoff handling in the calling stepFetch if the upstream is intermittently failing"],"exampleFix":"// before\nawait step({ type: 'fetch', url: 'https://api.example.com/v1/user' }); // 404\n// after\nawait step({ type: 'fetch', url: 'https://api.example.com/v1/users' });","handlingStrategy":"try-catch","validationCode":"const url = new URL(renderedUrl);\nif (!/^https?:$/.test(url.protocol)) throw new Error('bad url: ' + renderedUrl);","typeGuard":"function isFetchError(e: unknown): e is { code: 'FETCH_ERROR'; message: string } {\n  return typeof e === 'object' && e !== null && 'code' in e && (e as any).code === 'FETCH_ERROR';\n}","tryCatchPattern":"try {\n  const data = await stepFetch(cfg);\n} catch (e) {\n  if (isFetchError(e)) {\n    const status = parseInt(e.message.match(/HTTP (\\d+)/)?.[1] ?? '0', 10);\n    if (status === 429 || status >= 500) await retryWithBackoff();\n    else console.error('Non-retryable fetch failure:', e.message);\n  } else throw e;\n}","preventionTips":["Validate rendered URLs (protocol, host, path) before the fetch step runs","Keep API tokens in env/config with expiry tracking so 401s are rare","Add retry-with-backoff for 429/5xx statuses in stepFetch wrappers","Smoke-test endpoints with curl using the same headers before wiring them into the pipeline"],"tags":["http","network","fetch","http-status"],"backgroundTag":"http-non-2xx-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}