{"record":{"id":"a1ddf0ef92d3f069","repo":"payloadcms/payload","slug":"failed-to-upload-part-part-parttotal","errorCode":null,"errorMessage":"Failed to upload part ${part} / ${partTotal}","messagePattern":"Failed to upload part (.+?) / (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/storage-r2/src/client/R2ClientUploadHandler.ts","lineNumber":81,"sourceCode":"    params.multipartId = multipartUpload.uploadId\n    params.multipartKey = multipartUpload.key\n\n    const partTotal = Math.ceil(file.size / chunkSize)\n\n    for (let part = 1; part <= partTotal; part++) {\n      const bytesEnd = Math.min(part * chunkSize, file.size)\n      const bytesStart = (part - 1) * chunkSize\n\n      params.multipartNumber = String(part)\n\n      const body = file.slice(bytesStart, bytesEnd)\n      const headers = {\n        'Content-Length': String(body.size),\n        'Content-Type': 'application/octet-stream',\n      }\n      const uploaded = await fetch(getEndpoint(), { body, headers, method: 'POST' })\n      if (!uploaded.ok) {\n        throw new Error(`Failed to upload part ${part} / ${partTotal}`)\n      }\n\n      multipartUploadedParts.push((await uploaded.json()) as R2UploadedPart)\n\n      if (part === partTotal) {\n        delete params.multipartNumber\n\n        const body = JSON.stringify(multipartUploadedParts)\n        const headers = { 'Content-Type': 'application/json' }\n        const complete = await fetch(getEndpoint(), { body, headers, method: 'POST' })\n        if (!complete.ok) {\n          throw new Error(`Failed to complete multipart upload`)\n        }\n\n        const key = await complete.text()\n        return {\n          key,\n          prefix: sanitizedDocPrefix,","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/payloadcms/payload/blob/00c58b35c0ed348ddc22daabf467b139727214fd/packages/storage-r2/src/client/R2ClientUploadHandler.ts#L63-L99","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst uploaded = await fetch(getEndpoint(), { body, headers, method: 'POST' })\nif (!uploaded.ok) {\n  throw new Error(`Failed to upload part ${part} / ${partTotal}`)\n}\n\n// after — retry transient part failures\nasync function uploadPart(part: number, body: Blob, headers: Record<string, string>) {\n  for (let attempt = 0; attempt < 3; attempt++) {\n    const res = await fetch(getEndpoint(), { body, headers, method: 'POST' })\n    if (res.ok) return res\n    if (res.status >= 400 && res.status < 500 && res.status !== 429) {\n      throw new Error(`Failed to upload part ${part}/${partTotal} (${res.status})`)\n    }\n    await new Promise((r) => setTimeout(r, 2 ** attempt * 500))\n  }\n  throw new Error(`Failed to upload part ${part}/${partTotal} after retries`)\n}","handlingStrategy":"retry","validationCode":"function chunkSizeFitsWindow(args: { fileSize: number; chunkSize: number; signedUrlTtlMs: number; bytesPerMs: number }): boolean {\n  const parts = Math.ceil(args.fileSize / args.chunkSize)\n  const estimatedMs = (parts * args.chunkSize) / args.bytesPerMs\n  return estimatedMs < args.signedUrlTtlMs\n}","typeGuard":"function isPartUploadFailure(err: unknown): err is Error {\n  return err instanceof Error && /upload part/i.test(err.message)\n}","tryCatchPattern":"for (const part of parts) {\n  let success = false\n  for (let attempt = 0; attempt < 3 && !success; attempt++) {\n    try {\n      await uploadPart(part)\n      success = true\n    } catch (err) {\n      if (attempt === 2 || !isPartUploadFailure(err)) throw err\n      await backoff(attempt)\n    }\n  }\n}","preventionTips":["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."],"tags":["r2-storage","upload","client","network","multipart","retry"],"backgroundTag":null,"analyzedSha":"00c58b35c0ed348ddc22daabf467b139727214fd","analyzedAt":"2026-08-12T20:45:03.758Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}