payloadcms/payload · error
Failed to complete multipart upload
Error message
Failed to complete multipart upload
What it means
A plain Error thrown client-side in R2ClientUploadHandler when the final multipart-complete POST (sending the uploadedParts JSON array) returns non-ok. This is the commit step that finalizes the R2 multipart upload; failure here means the object was not assembled.
Source
Thrown at packages/storage-r2/src/client/R2ClientUploadHandler.ts:93
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
- Validate each multipartUploadedParts entry has a valid partNumber and ETag before sending the complete request.
- Read the complete response body/status to distinguish R2 part-list rejection from auth/network.
- Ensure the upload completes within the R2 multipart-upload lifetime (initiate → complete window).
- On failure, call the abort endpoint to clean up the dangling multipart upload.
Example fix
// before
const complete = await fetch(getEndpoint(), { body, headers, method: 'POST' })
if (!complete.ok) {
throw new Error(`Failed to complete multipart upload`)
}
// after — validate parts and surface R2's error
multipartUploadedParts.sort((a, b) => a.partNumber - b.partNumber)
const complete = await fetch(getEndpoint(), { body: JSON.stringify(multipartUploadedParts), headers, method: 'POST' })
if (!complete.ok) {
const detail = await complete.text().catch(() => '<no body>')
throw new Error(`Failed to complete multipart upload (${complete.status}): ${detail}`)
} Defensive patterns
Strategy: validation
Validate before calling
function partsAreComplete(parts: { partNumber: number; etag: string }[]): boolean {
if (!parts.length) return false
const numbers = parts.map((p) => p.partNumber).sort((a, b) => a - b)
return numbers.every((n, i) => n === i + 1) && parts.every((p) => Boolean(p.etag))
}
if (!partsAreComplete(multipartUploadedParts)) {
throw new Error('Cannot complete upload: parts missing or invalid')
} Type guard
function isCompleteFailure(err: unknown): err is Error {
return err instanceof Error && /complete multipart/i.test(err.message)
} Try / catch
try {
await completeMultipartUpload(multipartUploadedParts)
} catch (err) {
if (isCompleteFailure(err)) {
await abortMultipartUpload(uploadId) // cleanup dangling upload
}
throw err
} Prevention
- Validate every part has a partNumber and ETag before sending the complete request.
- Sort parts by partNumber before commit.
- Always abort the multipart upload on completion failure to avoid R2 storage charges.
When it happens
Trigger: The complete POST returns non-ok: R2 rejecting the parts list (e.g. missing/invalid ETag, wrong part numbers, part count mismatch), auth revoked, or network drop at the final step.
Common situations: An earlier part silently returned bad JSON that was pushed into multipartUploadedParts; the parts array is empty or out of order; signed-URL/uploadId expired before commit; worker timeout on large part lists.
Related errors
- Failed to initialize multipart upload
- Failed to upload part ${part} / ${partTotal}
- 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/f7e15805dd4a979d.
Report an issue: GitHub.