{"record":{"id":"89bdb425b38fc54d","repo":"hcengineering/platform","slug":"failed-to-parse-response-for-part-partnumber","errorCode":null,"errorMessage":"Failed to parse response for part ${partNumber}","messagePattern":"Failed to parse response for part (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"foundations/core/packages/storage-client/src/upload.ts","lineNumber":242,"sourceCode":"  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(),\n      method: 'PUT',\n      headers,\n      body: blob\n    },\n    options\n  )\n\n  try {\n    const response = JSON.parse(result.responseText)\n    return { etag: response.etag }\n  } catch (err) {\n    throw new Error(`Failed to parse response for part ${partNumber}`)\n  }\n}\n\nasync function multipartUploadAbort (baseUrl: string, headers: Record<string, string>, uploadId: string): Promise<void> {\n  const url = new URL(concatLink(baseUrl, '/abort'))\n  url.searchParams.set('uploadId', uploadId)\n\n  const response = await fetch(url, {\n    method: 'POST',\n    headers\n  })\n\n  if (!response.ok) {\n    throw new Error('Failed to reject multipart upload')\n  }\n}\n","sourceCodeStart":224,"sourceCodeEnd":259,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/foundations/core/packages/storage-client/src/upload.ts#L224-L259","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Log result.status and result.responseText at the throw site to see what the server actually returned for that part.","Check intermediate proxies/CDNs for HTML or empty 2xx responses on /part and bypass them for uploads.","Update the storage service so /part returns JSON {etag: \"...\"} on success.","Retry the failed part: multipart part uploads are idempotent for the same partNumber."],"exampleFix":"// before: opaque error, no diagnostic\nthrow new Error(`Failed to parse response for part ${partNumber}`)\n\n// after: surface what came back\nthrow new Error(`Failed to parse response for part ${partNumber}: status=${result.status} body=${result.responseText.slice(0, 200)}`)","handlingStrategy":"retry","validationCode":"// inspect the XHR result before trusting JSON.parse\nfunction looksLikeEtagResponse (r: { status: number, responseText: string }): boolean {\n  return r.status >= 200 && r.status < 300 && r.responseText.trim().startsWith('{') && r.responseText.includes('etag')\n}","typeGuard":"function hasEtag (v: unknown): v is { etag: string } {\n  return typeof v === 'object' && v !== null && typeof (v as any).etag === 'string'\n}\nconst parsed: unknown = JSON.parse(result.responseText)\nif (!hasEtag(parsed)) throw new Error('missing etag')","tryCatchPattern":"for (let attempt = 0; attempt < 3; attempt++) {\n  try { return await multipartUploadPart(url, headers, uploadId, partNumber, blob, opts) }\n  catch (err) {\n    if (err instanceof Error && err.message.startsWith('Failed to parse response for part')) {\n      await new Promise(r => setTimeout(r, 2 ** attempt * 500)); continue // part PUTs are idempotent\n    }\n    throw err\n  }\n}","preventionTips":["Bypass HTML-returning proxies/CDNs for upload part routes.","Confirm the storage service returns JSON {etag} with 2xx on /part.","Log raw responseText on parse failure for fast diagnosis.","Retry the specific part — same partNumber PUT is safe to repeat."],"tags":["json","parsing","upload","multipart","network"],"backgroundTag":"unexpected-response-body","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}