{"record":{"id":"f7e15805dd4a979d","repo":"payloadcms/payload","slug":"failed-to-complete-multipart-upload","errorCode":null,"errorMessage":"Failed to complete multipart upload","messagePattern":"Failed to complete multipart upload","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/storage-r2/src/client/R2ClientUploadHandler.ts","lineNumber":93,"sourceCode":"      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,\n        }\n      }\n    }\n  },\n})\n","sourceCodeStart":75,"sourceCodeEnd":105,"githubUrl":"https://github.com/payloadcms/payload/blob/00c58b35c0ed348ddc22daabf467b139727214fd/packages/storage-r2/src/client/R2ClientUploadHandler.ts#L75-L105","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst complete = await fetch(getEndpoint(), { body, headers, method: 'POST' })\nif (!complete.ok) {\n  throw new Error(`Failed to complete multipart upload`)\n}\n\n// after — validate parts and surface R2's error\nmultipartUploadedParts.sort((a, b) => a.partNumber - b.partNumber)\nconst complete = await fetch(getEndpoint(), { body: JSON.stringify(multipartUploadedParts), headers, method: 'POST' })\nif (!complete.ok) {\n  const detail = await complete.text().catch(() => '<no body>')\n  throw new Error(`Failed to complete multipart upload (${complete.status}): ${detail}`)\n}","handlingStrategy":"validation","validationCode":"function partsAreComplete(parts: { partNumber: number; etag: string }[]): boolean {\n  if (!parts.length) return false\n  const numbers = parts.map((p) => p.partNumber).sort((a, b) => a - b)\n  return numbers.every((n, i) => n === i + 1) && parts.every((p) => Boolean(p.etag))\n}\n\nif (!partsAreComplete(multipartUploadedParts)) {\n  throw new Error('Cannot complete upload: parts missing or invalid')\n}","typeGuard":"function isCompleteFailure(err: unknown): err is Error {\n  return err instanceof Error && /complete multipart/i.test(err.message)\n}","tryCatchPattern":"try {\n  await completeMultipartUpload(multipartUploadedParts)\n} catch (err) {\n  if (isCompleteFailure(err)) {\n    await abortMultipartUpload(uploadId) // cleanup dangling upload\n  }\n  throw err\n}","preventionTips":["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."],"tags":["r2-storage","upload","client","network","multipart"],"backgroundTag":null,"analyzedSha":"00c58b35c0ed348ddc22daabf467b139727214fd","analyzedAt":"2026-08-12T20:45:03.758Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}