{"record":{"id":"528eb963b865434b","repo":"hcengineering/platform","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":"foundations/core/packages/storage-client/src/upload.ts","lineNumber":212,"sourceCode":"  uploadId: string,\n  parts: Array<{ partNumber: number, etag: string }>,\n  signal?: AbortSignal\n): Promise<void> {\n  const url = new URL(concatLink(baseUrl, '/complete'))\n  url.searchParams.set('uploadId', uploadId)\n\n  const response = await fetch(url, {\n    signal,\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      ...headers\n    },\n    body: JSON.stringify({ parts })\n  })\n\n  if (!response.ok) {\n    throw new Error('Failed to complete multipart upload')\n  }\n}\n\nasync function multipartUploadPart (\n  baseUrl: string,\n  headers: Record<string, string>,\n  uploadId: string,\n  partNumber: number,\n  blob: Blob,\n  options?: FileStorageUploadOptions\n): Promise<{ etag: string }> {\n  const url = new URL(concatLink(baseUrl, '/part'))\n  url.searchParams.set('uploadId', uploadId)\n  url.searchParams.set('partNumber', `${partNumber}`)\n\n  const result = await uploadXhr(\n    {\n      url: url.toString(),","sourceCodeStart":194,"sourceCodeEnd":230,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/foundations/core/packages/storage-client/src/upload.ts#L194-L230","documentation":"multipartUploadComplete POSTs the collected parts list (partNumber + etag pairs) to `<baseUrl>/complete?uploadId=...`. If the server responds non-ok, it throws 'Failed to complete multipart upload'. At this point all 5MB parts were uploaded, so this failure means the server refused to finalize the object.","triggerScenarios":"All parts uploaded but POST /complete returns non-ok: 400 (parts list invalid, out of order, or a missing etag), 404 (uploadId expired/aborted server-side or wrong), 401/403 (token expired during a long upload), 409/500 on server-side object assembly.","commonSituations":"Very large files taking long enough for the token or the server-side multipart session to expire; a part retried and registered with a duplicate partNumber; proxy timing out on the complete call; uploadId mismatch because baseUrl changed mid-upload.","solutions":["Log response.status and body from /complete to get the server's reason (invalid parts vs expired uploadId vs auth).","Ensure the token is refreshed or long-lived enough to outlast the entire multipart transfer.","Verify parts are pushed in strictly ascending partNumber order, each with a non-empty etag, before complete is called.","On failure, restart the whole uploadMultipart (the library aborts the old session automatically) rather than retrying /complete alone."],"exampleFix":"// before: long upload with a token that expires mid-transfer\nawait uploadMultipart({ url, headers: { Authorization: `Bearer ${token}` }, body: hugeFile })\n\n// after: keep auth fresh across the transfer\nconst headers = () => ({ Authorization: `Bearer ${getFreshToken()}` })\nawait uploadMultipart({ url, headers: headers(), body: hugeFile }) // refresh token before starting long uploads","handlingStrategy":"retry","validationCode":"// ensure parts are complete and ordered before finalize (library does this, but callers wrapping it can check)\nconst ordered = parts.every((p, i) => p.partNumber === i + 1 && typeof p.etag === 'string' && p.etag.length > 0)\nif (!ordered) throw new Error('Invalid parts list before complete')","typeGuard":"function isValidParts (v: unknown): v is Array<{ partNumber: number, etag: string }> {\n  return Array.isArray(v) && v.every(p => typeof p?.partNumber === 'number' && typeof p?.etag === 'string')\n}","tryCatchPattern":"try {\n  await uploadMultipart({ url, headers, body: file }, { signal })\n} catch (err) {\n  if (err instanceof Error && err.message === 'Failed to complete multipart upload') {\n    // whole-session retry: abort happened server-side, restart uploadMultipart from scratch\n    await uploadMultipart({ url, headers: refreshedHeaders(), body: file }, { signal })\n  } else throw err\n}","preventionTips":["Use a long-lived or refreshed token for uploads that will outlast the transfer.","Retry the entire multipart session, not just the /complete call.","Keep part uploads strictly sequential (as the library does) to keep partNumbers ordered.","Check proxy/ingress timeouts exceed the expected transfer duration."],"tags":["http","upload","multipart","network"],"backgroundTag":"multipart-upload-complete-failed","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}