payloadcms/payload · error
Failed to upload part ${part} / ${partTotal}
Error message
Failed to upload part ${part} / ${partTotal} What it means
A plain Error thrown client-side in R2ClientUploadHandler during the per-chunk upload loop, when the POST of an individual part returns non-ok. The message interpolates the current part number and total part count. No status code is attached; it is a browser/worker fetch failure.
Source
Thrown at packages/storage-r2/src/client/R2ClientUploadHandler.ts:81
params.multipartId = multipartUpload.uploadId
params.multipartKey = multipartUpload.key
const partTotal = Math.ceil(file.size / chunkSize)
for (let part = 1; part <= partTotal; part++) {
const bytesEnd = Math.min(part * chunkSize, file.size)
const bytesStart = (part - 1) * chunkSize
params.multipartNumber = String(part)
const body = file.slice(bytesStart, bytesEnd)
const headers = {
'Content-Length': String(body.size),
'Content-Type': 'application/octet-stream',
}
const uploaded = await fetch(getEndpoint(), { body, headers, method: 'POST' })
if (!uploaded.ok) {
throw new Error(`Failed to upload part ${part} / ${partTotal}`)
}
multipartUploadedParts.push((await uploaded.json()) as R2UploadedPart)
if (part === partTotal) {
delete params.multipartNumber
const body = JSON.stringify(multipartUploadedParts)
const headers = { 'Content-Type': 'application/json' }
const complete = await fetch(getEndpoint(), { body, headers, method: 'POST' })
if (!complete.ok) {
throw new Error(`Failed to complete multipart upload`)
}
const key = await complete.text()
return {
key,
prefix: sanitizedDocPrefix,View on GitHub (pinned to 00c58b35c0)
Solutions
- Inspect the network response status for the failing part to distinguish auth (403) from network (0/5xx).
- Implement client-side retry with exponential backoff for transient part failures before aborting the whole upload.
- Reduce chunk size or total part count to keep the upload window shorter than the signed-URL lifetime.
- Re-authenticate / refresh credentials before retrying if the part returned 401/403.
Example fix
// before
const uploaded = await fetch(getEndpoint(), { body, headers, method: 'POST' })
if (!uploaded.ok) {
throw new Error(`Failed to upload part ${part} / ${partTotal}`)
}
// after — retry transient part failures
async function uploadPart(part: number, body: Blob, headers: Record<string, string>) {
for (let attempt = 0; attempt < 3; attempt++) {
const res = await fetch(getEndpoint(), { body, headers, method: 'POST' })
if (res.ok) return res
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
throw new Error(`Failed to upload part ${part}/${partTotal} (${res.status})`)
}
await new Promise((r) => setTimeout(r, 2 ** attempt * 500))
}
throw new Error(`Failed to upload part ${part}/${partTotal} after retries`)
} Defensive patterns
Strategy: retry
Validate before calling
function chunkSizeFitsWindow(args: { fileSize: number; chunkSize: number; signedUrlTtlMs: number; bytesPerMs: number }): boolean {
const parts = Math.ceil(args.fileSize / args.chunkSize)
const estimatedMs = (parts * args.chunkSize) / args.bytesPerMs
return estimatedMs < args.signedUrlTtlMs
} Type guard
function isPartUploadFailure(err: unknown): err is Error {
return err instanceof Error && /upload part/i.test(err.message)
} Try / catch
for (const part of parts) {
let success = false
for (let attempt = 0; attempt < 3 && !success; attempt++) {
try {
await uploadPart(part)
success = true
} catch (err) {
if (attempt === 2 || !isPartUploadFailure(err)) throw err
await backoff(attempt)
}
}
} Prevention
- Retry transient part failures with backoff before aborting the whole upload.
- Keep total upload time shorter than the signed-URL lifetime by tuning chunk size.
- Refresh credentials before retrying on 401/403.
When it happens
Trigger: Mid-upload chunk POST fails: network drop, R2 presigned/signed window expiring, the handler rejecting the part (auth revoked mid-session), or the worker hitting a CPU/subrequest limit.
Common situations: Large files split into many parts where the network flickers; long uploads outlasting the signed-URL validity; mobile/flaky connections; Cloudflare Worker subrequest limits; user logged out during upload.
Related errors
- Failed to initialize multipart upload
- Failed to complete multipart upload
- Collection ${collectionSlug} was not found in R2 Storage opt
- Fetch failed with status: ${response.status}
- Too many redirects.
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/a1ddf0ef92d3f069.
Report an issue: GitHub.