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

  1. Replace the chained URL with its final, direct URL before storing it on the document.
  2. Fix the origin server's redirect configuration (eliminate loops or redundant hops).
  3. Host the file on a single stable URL (direct bucket/CDN link).
  4. If you control the fetch, consider pre-resolving the URL with a redirect-following fetch and storing the final destination.
  5. 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

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


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/a5430d362ce4b3de. Report an issue: GitHub.