santifer/career-ops · error · Error
too many redirects (> ) for
Error message
too many redirects (>${MAX_REDIRECTS}) for ${url} What it means
The guarded fetch wrapper follows up to MAX_REDIRECTS redirects, re-validating each hop against the egress policy (hostOk/SSRF checks). If it exceeds that limit, it gives up and throws this error naming the original URL — protecting against redirect loops and redirect-based sandbox escapes.
Solutions
- Fetch the final URL directly instead of following the redirect chain
- Fix the server's redirect loop (check http→https and www→apex rules)
- Increase MAX_REDIRECTS only if the chain is legitimately long — prefer not to, since it loosens the sandbox guard
- Test the URL with curl -IL to see the redirect chain and where it loops
Example fix
// before
await ctx.fetch('http://bit.ly/old-link'); // loops past MAX_REDIRECTS
// Error: too many redirects (>3) for http://bit.ly/old-link
// after: resolve the chain once, use the final destination
await ctx.fetch('https://example.com/final/destination'); Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight: resolve the redirect chain outside the sandbox limit
// curl -sIL -o /dev/null -w '%{url_effective}' <url> → use the effective URL
const finalUrl = await resolveRedirectsOnce(url); // via fetch with redirect:'manual', bounded
await ctx.fetch(finalUrl); Type guard
null
Try / catch
try { return await ctx.fetch(url); } catch (e) { if (String(e.message).startsWith('too many redirects')) { /* resolve the final URL once, then retry against it */ } throw e; } Prevention
- Store final destination URLs, not short-links, in plugin configs
- Fix http↔https and www/apex redirect loops on servers you control
- Avoid raising MAX_REDIRECTS — a long chain usually indicates a server bug
When it happens
Trigger: A plugin fetch whose target responds with a redirect chain longer than MAX_REDIRECTS, including redirect loops (A → B → A) or misconfigured servers bounce-looping between www/apex or http/https.
Common situations: A stale short-link or tracking URL in a redirect loop; a server with broken TLS redirecting http→https→http; a login page bouncing unauthenticated requests between endpoints.
Related errors
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/989b18ba357e978c.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/_engine.mjs:436
await resolveAndValidate(next.hostname, { allowsLocalhost });
if (next.hostname !== current.hostname) {
// Don't forward credentials across a hostname change (what the
// platform fetch does for cross-origin redirects; we do it manually).
reqHeaders = Object.fromEntries(Object.entries(reqHeaders).filter(([k]) => !/^(authorization|cookie)$/i.test(k)));
}
current = next;
continue;
}
if (!res.ok) {
const snippet = (await res.text().catch(() => '')).replace(/\s+/g, ' ').trim().slice(0, 300);
const err = new Error(snippet ? `HTTP ${res.status}: ${snippet}` : `HTTP ${res.status}`);
// @ts-ignore
err.status = res.status;
throw err;
}
return res;
}
throw new Error(`too many redirects (>${MAX_REDIRECTS}) for ${url}`);
};
}
/**
* Build the least-privilege ctx for a plugin. The scoped frozen env is a
* CONVENIENCE (process.env is still globally reachable from any module) — the
* real boundary is code review + trust.
* @param {PluginManifestNormalized} manifest
* @param {{ dryRun?: boolean, settings?: object }} [opts]
* @returns {PluginContext}
*/
export function buildCtx(manifest, opts = {}) {
const scoped = {};
for (const name of [...manifest.requiredEnv, ...manifest.optionalEnv]) {
if (process.env[name] !== undefined) scoped[name] = process.env[name];
}
const env = Object.freeze({ ...scoped });
// Secret values long enough to be worth redacting (avoid no-op/over-redactionView on GitHub (pinned to aac998c7ed)