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

  1. Inspect the network response status for the failing part to distinguish auth (403) from network (0/5xx).
  2. Implement client-side retry with exponential backoff for transient part failures before aborting the whole upload.
  3. Reduce chunk size or total part count to keep the upload window shorter than the signed-URL lifetime.
  4. 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

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


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