hcengineering/platform · error

Failed to complete multipart upload

Error message

Failed to complete multipart upload

What it means

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.

Source

Thrown at foundations/core/packages/storage-client/src/upload.ts:212

  uploadId: string,
  parts: Array<{ partNumber: number, etag: string }>,
  signal?: AbortSignal
): Promise<void> {
  const url = new URL(concatLink(baseUrl, '/complete'))
  url.searchParams.set('uploadId', uploadId)

  const response = await fetch(url, {
    signal,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...headers
    },
    body: JSON.stringify({ parts })
  })

  if (!response.ok) {
    throw new Error('Failed to complete multipart upload')
  }
}

async function multipartUploadPart (
  baseUrl: string,
  headers: Record<string, string>,
  uploadId: string,
  partNumber: number,
  blob: Blob,
  options?: FileStorageUploadOptions
): Promise<{ etag: string }> {
  const url = new URL(concatLink(baseUrl, '/part'))
  url.searchParams.set('uploadId', uploadId)
  url.searchParams.set('partNumber', `${partNumber}`)

  const result = await uploadXhr(
    {
      url: url.toString(),

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Log response.status and body from /complete to get the server's reason (invalid parts vs expired uploadId vs auth).
  2. Ensure the token is refreshed or long-lived enough to outlast the entire multipart transfer.
  3. Verify parts are pushed in strictly ascending partNumber order, each with a non-empty etag, before complete is called.
  4. On failure, restart the whole uploadMultipart (the library aborts the old session automatically) rather than retrying /complete alone.

Example fix

// before: long upload with a token that expires mid-transfer
await uploadMultipart({ url, headers: { Authorization: `Bearer ${token}` }, body: hugeFile })

// after: keep auth fresh across the transfer
const headers = () => ({ Authorization: `Bearer ${getFreshToken()}` })
await uploadMultipart({ url, headers: headers(), body: hugeFile }) // refresh token before starting long uploads
Defensive patterns

Strategy: retry

Validate before calling

// ensure parts are complete and ordered before finalize (library does this, but callers wrapping it can check)
const ordered = parts.every((p, i) => p.partNumber === i + 1 && typeof p.etag === 'string' && p.etag.length > 0)
if (!ordered) throw new Error('Invalid parts list before complete')

Type guard

function isValidParts (v: unknown): v is Array<{ partNumber: number, etag: string }> {
  return Array.isArray(v) && v.every(p => typeof p?.partNumber === 'number' && typeof p?.etag === 'string')
}

Try / catch

try {
  await uploadMultipart({ url, headers, body: file }, { signal })
} catch (err) {
  if (err instanceof Error && err.message === 'Failed to complete multipart upload') {
    // whole-session retry: abort happened server-side, restart uploadMultipart from scratch
    await uploadMultipart({ url, headers: refreshedHeaders(), body: file }, { signal })
  } else throw err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/528eb963b865434b. Report an issue: GitHub.