hcengineering/platform · error

Failed to parse response for part ${partNumber}

Error message

Failed to parse response for part ${partNumber}

What it means

After a 5MB part PUT via uploadXhr, multipartUploadPart JSON-parses result.responseText to extract the etag. If parsing fails (or the parsed body lacks an etag structure), it throws 'Failed to parse response for part N'. This happens even though uploadXhr only resolves on 2xx, meaning the server returned a 2xx with a non-JSON body.

Source

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

  const url = new URL(concatLink(baseUrl, '/part'))
  url.searchParams.set('uploadId', uploadId)
  url.searchParams.set('partNumber', `${partNumber}`)

  const result = await uploadXhr(
    {
      url: url.toString(),
      method: 'PUT',
      headers,
      body: blob
    },
    options
  )

  try {
    const response = JSON.parse(result.responseText)
    return { etag: response.etag }
  } catch (err) {
    throw new Error(`Failed to parse response for part ${partNumber}`)
  }
}

async function multipartUploadAbort (baseUrl: string, headers: Record<string, string>, uploadId: string): Promise<void> {
  const url = new URL(concatLink(baseUrl, '/abort'))
  url.searchParams.set('uploadId', uploadId)

  const response = await fetch(url, {
    method: 'POST',
    headers
  })

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

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Log result.status and result.responseText at the throw site to see what the server actually returned for that part.
  2. Check intermediate proxies/CDNs for HTML or empty 2xx responses on /part and bypass them for uploads.
  3. Update the storage service so /part returns JSON {etag: "..."} on success.
  4. Retry the failed part: multipart part uploads are idempotent for the same partNumber.

Example fix

// before: opaque error, no diagnostic
throw new Error(`Failed to parse response for part ${partNumber}`)

// after: surface what came back
throw new Error(`Failed to parse response for part ${partNumber}: status=${result.status} body=${result.responseText.slice(0, 200)}`)
Defensive patterns

Strategy: retry

Validate before calling

// inspect the XHR result before trusting JSON.parse
function looksLikeEtagResponse (r: { status: number, responseText: string }): boolean {
  return r.status >= 200 && r.status < 300 && r.responseText.trim().startsWith('{') && r.responseText.includes('etag')
}

Type guard

function hasEtag (v: unknown): v is { etag: string } {
  return typeof v === 'object' && v !== null && typeof (v as any).etag === 'string'
}
const parsed: unknown = JSON.parse(result.responseText)
if (!hasEtag(parsed)) throw new Error('missing etag')

Try / catch

for (let attempt = 0; attempt < 3; attempt++) {
  try { return await multipartUploadPart(url, headers, uploadId, partNumber, blob, opts) }
  catch (err) {
    if (err instanceof Error && err.message.startsWith('Failed to parse response for part')) {
      await new Promise(r => setTimeout(r, 2 ** attempt * 500)); continue // part PUTs are idempotent
    }
    throw err
  }
}

Prevention

When it happens

Trigger: PUT /part?uploadId=...&partNumber=N returns 2xx but the body is not JSON: empty response, an HTML error/maintenance page from a proxy that rewrote the status, a CORS-filtered empty body, or the server omitting the etag field.

Common situations: Nginx/ingress returning HTML 200 pages (custom error or captive portal); CDN caching a non-JSON response for /part; storage service deployed without the etag JSON response (older backend version); dev proxies (Vite/webpack) interfering with XHR responseText.

Understand the failure class

Related errors


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