payloadcms/payload · warning · APIError
Too many redirects (max ${maxRedirects})
Error message
Too many redirects (max ${maxRedirects}) What it means
When fetching a remote file (paste-URL / re-upload), Payload follows HTTP redirects manually with a hard cap of `maxRedirects = 3`. Each 3xx response increments `redirectCount`; once it exceeds 3, the loop aborts with `APIError` HTTP 403 `Too many redirects (max 3)`. The cap defends against redirect loops and chained-shorteners that could be abused for SSRF/time-exhaustion.
Source
Thrown at packages/payload/src/uploads/getExternalFile.ts:75
res = await fetch(fileURL, {
credentials: 'include',
headers,
method: 'GET',
redirect: 'manual',
})
} else {
// Default
res = await safeFetch(fileURL, {
credentials: 'include',
headers,
method: 'GET',
})
}
if (res.status >= 300 && res.status < 400) {
redirectCount++
if (redirectCount > maxRedirects) {
throw new APIError(`Too many redirects (max ${maxRedirects})`, 403)
}
const location = res.headers.get('location')
if (location) {
fileURL = new URL(location, fileURL).toString()
if (
uploadConfig.pasteURL &&
uploadConfig.pasteURL.allowList &&
!isURLAllowed(fileURL, uploadConfig.pasteURL.allowList)
) {
throw new APIError('Redirect target is not allowed.', 400)
}
continue
}
}
break
}
View on GitHub (pinned to 00c58b35c0)
Solutions
- Replace the chained URL with its final, direct URL before storing it on the document.
- Fix the origin server's redirect configuration (eliminate loops or redundant hops).
- Host the file on a single stable URL (direct bucket/CDN link).
- If you control the fetch, consider pre-resolving the URL with a redirect-following fetch and storing the final destination.
- Document and accept the 3-redirect ceiling when choosing storage providers.
Example fix
// before — stored URL is a multi-hop short link
doc.url = 'https://short.example/xY' // -> auth.example -> bucket.example/file.png
// after — store the resolved final URL
const res = await fetch('https://short.example/xY')
doc.url = res.url // 'https://bucket.example/file.png' Defensive patterns
Strategy: fallback
Validate before calling
const MAX_REDIRECTS = 3
async function resolveFinalUrl(start: string): Promise<string> {
let url = start, hops = 0, res = await fetch(url, { redirect: 'manual' })
while (res.status >= 300 && res.status < 400 && hops < MAX_REDIRECTS) {
const loc = res.headers.get('location')
if (!loc) break
url = new URL(loc, url).toString()
res = await fetch(url, { redirect: 'manual' })
hops++
}
if (hops >= MAX_REDIRECTS && res.status >= 300 && res.status < 400) {
throw new Error(`URL exceeded ${MAX_REDIRECTS} redirects; resolve manually`)
}
return url
}
const finalUrl = await resolveFinalUrl(doc.url) Type guard
function isRedirectLoop(start: string, chain: string[]): boolean {
return chain.filter((u) => u === start).length > 1
} Try / catch
try {
await payload.update({ collection: 'media', id, data: { url } })
} catch (err) {
if (err instanceof Error && /too many redirects/i.test(err.message)) {
// pre-resolve the URL to its final destination, store that, and retry
} else throw err
} Prevention
- Store direct final URLs, not short links or chains.
- Pre-resolve URLs with a manual-redirect fetch before persisting.
- Eliminate redirect loops on your own origins.
When it happens
Trigger: `getExternalFile` fetches a URL whose response chain returns more than three consecutive 3xx redirects before a final 2xx/4xx/5xx. Each redirect's `Location` is resolved relative to the current URL and re-fetched in the same loop.
Common situations: A CDN/storage provider chains through multiple hosts (e.g. short-link → auth gateway → bucket). A misconfigured origin returns a redirect loop (A→B→A). The pasted URL is a shortener that adds hops. A signed-URL provider redirects to a regional endpoint which redirects again.
Related errors
- Too many redirects.
- Failed to fetch file from ${fileURL}
- Failed to fetch the file from the provided URL.
- Redirect target is not allowed.
- Invalid file url
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/a5430d362ce4b3de.
Report an issue: GitHub.