payloadcms/payload · error · APIError

Failed to fetch file from ${fileURL}

Error message

Failed to fetch file from ${fileURL}

What it means

After the redirect loop resolves, `getExternalFile` checks `res.ok`. If the final response is missing (`res` null) or its status is not 2xx, Payload throws `APIError` with that status (or 500 if `res` is null). The thrown message is `Failed to fetch file from <fileURL>` and the HTTP status mirrors the upstream failure — so a 404 upstream surfaces as HTTP 404 here.

Source

Thrown at packages/payload/src/uploads/getExternalFile.ts:95

        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
    }

    if (!res || !res.ok) {
      throw new APIError(`Failed to fetch file from ${fileURL}`, res?.status)
    }

    const data = await res.arrayBuffer()

    return {
      name: filename,
      data: Buffer.from(data),
      mimetype: res.headers.get('content-type') || undefined!,
      size: Number(res.headers.get('content-length')) || 0,
    }
  }

  throw new APIError('Invalid file url', 400)
}

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Open the same URL in a browser/curl from the server to see the real upstream status code and body.
  2. For 404/410: the remote file is gone — re-upload or update the document's URL.
  3. For 403/401: ensure the fetch carries required auth (review `externalFileHeaderFilter` / cookie trimming) or use a publicly reachable URL.
  4. For 5xx: retry once the origin recovers, or mirror the file to your own storage.
  5. Confirm `uploadConfig.externalFileHeaderFilter` isn't dropping a header the upstream requires.

Example fix

// before — fetching a URL that 404s at runtime
await payload.update({ collection: 'media', id, file: { data: Buffer.alloc(0), name, mimetype, size } })
// where doc.url points to a deleted file

// after — verify reachability before re-upload
const ok = await fetch(doc.url).then(r => r.ok)
if (!ok) {
  await payload.update({ collection: 'media', id, data: { url: newUrl } })
}
Defensive patterns

Strategy: try-catch

Validate before calling

async function isFetchable(url: string): Promise<boolean> {
  try {
    const res = await fetch(url, { method: 'HEAD' })
    return res.ok
  } catch {
    return false
  }
}

if (!(await isFetchable(doc.url))) {
  // URL is not reachable — update it or upload bytes directly
}

Type guard

function isHttpOkStatus(status: number | undefined): boolean {
  return typeof status === 'number' && status >= 200 && status < 300
}

Try / catch

try {
  await payload.update({ collection: 'media', id, data: { url } })
} catch (err) {
  if (err instanceof Error && /failed to fetch file from/i.test(err.message)) {
    const status = (err as any).status
    if (status === 404) { /* file gone — re-upload */ }
    else if (status >= 500) { /* transient — retry with backoff */ }
    else { /* 4xx auth — fix headers */ }
  } else throw err
}

Prevention

When it happens

Trigger: `getExternalFile` completes the fetch/redirect loop with a non-2xx final status (404 Not Found, 403 Forbidden, 401, 500, 502, 503…), or `res` is somehow null/undefined after the loop. Triggered during duplication/re-upload of a remote file, or via a paste-URL upload.

Common situations: The stored/remote URL 404s (file deleted, bucket key changed, expired signed URL). The remote host returns 403 (permissions, hot-link protection, geo-block). The origin is down (5xx). Authentication headers were stripped (`trimAuthCookies`) and the upstream now rejects. Network/proxy returns a non-2xx gateway error.

Related errors


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