{"record":{"id":"a5430d362ce4b3de","repo":"payloadcms/payload","slug":"too-many-redirects-max-maxredirects","errorCode":null,"errorMessage":"Too many redirects (max ${maxRedirects})","messagePattern":"Too many redirects \\(max (.+?)\\)","errorType":"http","errorClass":"APIError","httpStatus":403,"severity":"warning","filePath":"packages/payload/src/uploads/getExternalFile.ts","lineNumber":75,"sourceCode":"        res = await fetch(fileURL, {\n          credentials: 'include',\n          headers,\n          method: 'GET',\n          redirect: 'manual',\n        })\n      } else {\n        // Default\n        res = await safeFetch(fileURL, {\n          credentials: 'include',\n          headers,\n          method: 'GET',\n        })\n      }\n\n      if (res.status >= 300 && res.status < 400) {\n        redirectCount++\n        if (redirectCount > maxRedirects) {\n          throw new APIError(`Too many redirects (max ${maxRedirects})`, 403)\n        }\n        const location = res.headers.get('location')\n        if (location) {\n          fileURL = new URL(location, fileURL).toString()\n          if (\n            uploadConfig.pasteURL &&\n            uploadConfig.pasteURL.allowList &&\n            !isURLAllowed(fileURL, uploadConfig.pasteURL.allowList)\n          ) {\n            throw new APIError('Redirect target is not allowed.', 400)\n          }\n          continue\n        }\n      }\n\n      break\n    }\n","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/payloadcms/payload/blob/00c58b35c0ed348ddc22daabf467b139727214fd/packages/payload/src/uploads/getExternalFile.ts#L57-L93","documentation":"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.","triggerScenarios":"`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.","commonSituations":"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.","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."],"exampleFix":"// before — stored URL is a multi-hop short link\ndoc.url = 'https://short.example/xY' // -> auth.example -> bucket.example/file.png\n\n// after — store the resolved final URL\nconst res = await fetch('https://short.example/xY')\ndoc.url = res.url // 'https://bucket.example/file.png'","handlingStrategy":"fallback","validationCode":"const MAX_REDIRECTS = 3\nasync function resolveFinalUrl(start: string): Promise<string> {\n  let url = start, hops = 0, res = await fetch(url, { redirect: 'manual' })\n  while (res.status >= 300 && res.status < 400 && hops < MAX_REDIRECTS) {\n    const loc = res.headers.get('location')\n    if (!loc) break\n    url = new URL(loc, url).toString()\n    res = await fetch(url, { redirect: 'manual' })\n    hops++\n  }\n  if (hops >= MAX_REDIRECTS && res.status >= 300 && res.status < 400) {\n    throw new Error(`URL exceeded ${MAX_REDIRECTS} redirects; resolve manually`)\n  }\n  return url\n}\n\nconst finalUrl = await resolveFinalUrl(doc.url)","typeGuard":"function isRedirectLoop(start: string, chain: string[]): boolean {\n  return chain.filter((u) => u === start).length > 1\n}","tryCatchPattern":"try {\n  await payload.update({ collection: 'media', id, data: { url } })\n} catch (err) {\n  if (err instanceof Error && /too many redirects/i.test(err.message)) {\n    // pre-resolve the URL to its final destination, store that, and retry\n  } else throw err\n}","preventionTips":["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."],"tags":["upload","redirects","network","external-file"],"backgroundTag":null,"analyzedSha":"00c58b35c0ed348ddc22daabf467b139727214fd","analyzedAt":"2026-08-12T20:45:03.758Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}