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
- Open the same URL in a browser/curl from the server to see the real upstream status code and body.
- For 404/410: the remote file is gone — re-upload or update the document's URL.
- For 403/401: ensure the fetch carries required auth (review `externalFileHeaderFilter` / cookie trimming) or use a publicly reachable URL.
- For 5xx: retry once the origin recovers, or mirror the file to your own storage.
- 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
- Verify remote URLs return 2xx before persisting or re-uploading.
- Keep signed URLs fresh; refresh before they expire.
- Ensure required auth headers survive `externalFileHeaderFilter`.
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
- Too many redirects (max ${maxRedirects})
- Too many redirects.
- Failed to fetch the file from the provided URL.
- Invalid file url
- Blocked unsafe attempt to ${hostname}
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/438d6f3d80f3b689.
Report an issue: GitHub.