{"record":{"id":"43c322784b3775f2","repo":"JuliusBrussee/caveman","slug":"fetch-failed-unexpected-redirect","errorCode":null,"errorMessage":"fetch failed: unexpected redirect","messagePattern":"fetch failed: unexpected redirect","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/cli/src/proxy-fetch.ts","lineNumber":353,"sourceCode":"      body: buffered,\n      signal: request.signal ?? null,\n    };\n    let hop = resolveProxyUrl(current.url, env);\n\n    for (let redirects = 0; ; redirects += 1) {\n      const response = hop ? await sendThroughProxy(current, hop) : await baseFetch(current.url, {\n        method: current.method,\n        headers: current.headers as HeadersInit,\n        body: asBodyInit(current.body),\n        signal: current.signal,\n        redirect: \"manual\",\n      });\n\n      const location = response.headers.get(\"location\");\n      const redirectable = [301, 302, 303, 307, 308].includes(response.status);\n      if (!redirectable || !location || request.redirect === \"manual\") return response;\n      if (redirects >= MAX_REDIRECTS) throw new TypeError(\"fetch failed: too many redirects\");\n      if (request.redirect === \"error\") throw new TypeError(\"fetch failed: unexpected redirect\");\n\n      const next = new URL(location, current.url);\n      const method = nextMethod(response.status, current.method);\n      const bodyDropped = method !== current.method;\n      void response.body?.cancel();\n      current = {\n        method,\n        url: next,\n        headers: redirectedHeaders(current.headers, current.url, next, bodyDropped),\n        body: bodyDropped ? null : current.body,\n        signal: current.signal,\n      };\n      hop = resolveProxyUrl(next, env);\n    }\n  } as typeof fetch;\n}\n\n/**","sourceCodeStart":335,"sourceCodeEnd":371,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/5184b3d11ac6a1acb7d44b9bfaa31698157cff97/packages/cli/src/proxy-fetch.ts#L335-L371","documentation":"When the caller passes redirect: \"error\", the proxy-aware fetch treats any redirectable 3xx response as a hard failure instead of following it, throwing this TypeError. It mirrors the WHATWG fetch spec's 'error' redirect mode.","triggerScenarios":"Calling fetch (installed by installProxyAwareFetch) with redirect: \"error\" while the server responds 301/302/303/307/308 with a Location header.","commonSituations":"Hardening-sensitive code that opts into redirect:\"error\" then hits an endpoint that has moved (http->https upgrade, trailing-slash redirect, renamed API route); stale bookmarks or config pointing at old URLs.","solutions":["Update the request URL to the current (non-redirecting) endpoint.","Remove redirect: \"error\" (default is \"follow\") if automatic redirect handling is acceptable.","Follow the redirect manually with redirect: \"manual\" and inspect response.headers.get(\"location\").","Ask the server owner to stop redirecting that route if it should be stable."],"exampleFix":"// before\nawait fetch(\"http://api.example.com/v1/things\", { redirect: \"error\" }); // 301 to https\n// after\nawait fetch(\"https://api.example.com/v1/things\", { redirect: \"error\" }); // use the final URL","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isUnexpectedRedirect(e: unknown): boolean { return e instanceof TypeError && e.message.includes(\"unexpected redirect\"); }","tryCatchPattern":"try {\n  const res = await fetch(url, { redirect: \"error\" });\n} catch (e) {\n  if (e instanceof TypeError && e.message.includes(\"unexpected redirect\")) {\n    console.error(`${url} redirected; update to the final URL or allow redirects`);\n  } else throw e;\n}","preventionTips":["Use redirect: \"error\" only on URLs you control and have pinned to final destinations.","Periodically curl endpoints to detect new redirects after server changes.","Default to redirect: \"follow\" unless strict no-redirect semantics are required.","Update hardcoded URLs after http->https migrations."],"tags":["network","http","redirect","fetch"],"backgroundTag":"unexpected-redirect","analyzedSha":"5184b3d11ac6a1acb7d44b9bfaa31698157cff97","analyzedAt":"2026-08-31T22:10:17.934Z","contentChangedAt":"2026-08-31T22:10:17.934Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}