payloadcms/payload · error · APIError

Too many redirects.

Error message

Too many redirects.

What it means

APIError (HTTP 403, 'Too many redirects.') thrown when following the redirect chain from the remote server exceeds maxRedirects (3). The handler follows 3xx responses manually (redirect: 'manual'), incrementing redirectCount each time, and aborts when it would exceed 3.

Source

Thrown at packages/payload/src/uploads/endpoints/getFileFromURL.ts:93

      // Allow-listed URLs bypass SSRF filtering (e.g. internal/localhost CDNs)
      response = await fetch(fileURL, {
        headers: { 'Accept-Encoding': 'identity' },
        redirect: 'manual',
        signal: AbortSignal.timeout(30_000),
      })
    } else {
      response = await safeFetch(fileURL, {
        headers: {
          'Accept-Encoding': 'identity',
        },
        signal: AbortSignal.timeout(30_000),
      })
    }

    if (response.status >= 300 && response.status < 400) {
      redirectCount++
      if (redirectCount > maxRedirects) {
        throw new APIError('Too many redirects.', 403)
      }
      const location = response.headers.get('location')
      if (location) {
        fileURL = new URL(location, fileURL).href
        if (hasAllowList && !isURLAllowed(fileURL, config.upload.pasteURL.allowList)) {
          throw new APIError('The provided URL is not allowed.', 400)
        }
        continue
      }
    }

    break
  }

  if (!response.ok) {
    throw new APIError('Failed to fetch the file from the provided URL.', response.status)
  }

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Use a direct, canonical URL with no redirects (resolve the chain once and store the final URL).
  2. Fix the redirect loop on the origin server.
  3. Pre-warm/resolve the URL client-side and pass the final destination as src.
  4. If you legitimately need more hops, fork the endpoint (maxRedirects is hard-coded to 3).

Example fix

// client — before
fetch(`/api/media/paste-url?src=${encodeURIComponent(shortUrl)}`)
// after — resolve redirects first, send final URL
const finalUrl = await resolveRedirects(shortUrl, { max: 5 })
fetch(`/api/media/paste-url?src=${encodeURIComponent(finalUrl)}`)
Defensive patterns

Strategy: retry

Validate before calling

async function finalUrlAfterRedirects(u: string, max = 3): Promise<string> {
  let cur = u, n = 0
  while (n <= max) {
    const r = await fetch(cur, { redirect: 'manual' })
    if (r.status >= 300 && r.status < 400 && r.headers.get('location')) {
      cur = new URL(r.headers.get('location')!, cur).href; n++
    } else return cur
  }
  throw new Error('Too many redirects')
}
const finalSrc = await finalUrlAfterRedirects(src)
// send finalSrc as src to the endpoint

Type guard

const isRedirectStatus = (s: number): boolean => s >= 300 && s < 400

Try / catch

try {
  await fetch(`/api/media/paste-url?src=${encodeURIComponent(src)}`, { method: 'POST' })
} catch (e) {
  if (/Too many redirects/.test(e.message)) {
    // resolve upstream and retry once with the final URL
    const final = await resolveRedirects(src)
    await fetch(`/api/media/paste-url?src=${encodeURIComponent(final)}`, { method: 'POST' })
  }
}

Prevention

When it happens

Trigger: The remote src URL (or any redirect hop) returns 3xx more than 3 times in a row — e.g. a redirect loop, a CDN chain, or a URL shortener stacking hops. Each Location header is resolved and re-fetched; on the 4th redirect the handler throws.

Common situations: Redirect loop on the origin (A→B→A); a chain through multiple CDNs/login-gates; an HTTP→HTTPS→www hop sequence exceeding 3; a host that redirects to a session URL that itself redirects.

Related errors


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