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

  1. Validate each multipartUploadedParts entry has a valid partNumber and ETag before sending the complete request.
  2. Read the complete response body/status to distinguish R2 part-list rejection from auth/network.
  3. Ensure the upload completes within the R2 multipart-upload lifetime (initiate → complete window).
  4. 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

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


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