{"record":{"id":"339f8e83e936f981","repo":"hcengineering/platform","slug":"missing-body","errorCode":null,"errorMessage":"missing body","messagePattern":"missing body","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"services/datalake/pod-datalake/src/handlers/multipart.ts","lineNumber":81,"sourceCode":"  res: Response,\n  datalake: Datalake\n): Promise<void> {\n  const workspace = req.params.workspace as WorkspaceUuid\n  const partNumber = req.query.partNumber as string\n\n  let uuid: string\n  let uploadId: string\n\n  const upload = req.query.uploadId as string\n  try {\n    ;({ uuid, uploadId } = parseUploadId(upload))\n  } catch (err: any) {\n    res.status(400).send('invalid upload id')\n    return\n  }\n\n  if (!req.readable) {\n    res.status(400).send('missing body')\n    return\n  }\n\n  const { bucket } = await datalake.selectStorage(ctx, workspace)\n\n  const part = await bucket.uploadMultipartPart(ctx, uuid, { uploadId }, req, {\n    size: Number.parseInt(req.headers['content-length'] ?? '0'),\n    partNumber: Number.parseInt(partNumber)\n  })\n\n  ctx.info('multipart-part', { workspace, uuid, uploadId, partNumber })\n\n  res.status(200).json(part)\n}\n\nexport async function handleMultipartUploadComplete (\n  ctx: MeasureContext,\n  req: Request,","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/services/datalake/pod-datalake/src/handlers/multipart.ts#L63-L99","documentation":"handleMultipartUploadPart streams the request body as the part data. If req.readable is false — the body was already consumed, is empty, or the request was not sent with a streaming body — the handler responds 400 'missing body'. The part bytes themselves are the payload, so an unreadable body cannot be processed.","triggerScenarios":"Sending the part-upload request with an empty body, with Content-Length 0, or after another middleware/consumer already drained the stream; using a JSON body wrapper instead of raw binary body.","commonSituations":"HTTP clients that buffer/transform the body; proxies that reject or strip the body on chunked uploads; retry logic re-sending a consumed request object.","solutions":["Send the raw part bytes as the request body (do not wrap in JSON) and ensure the stream is not pre-consumed.","Check no middleware reads req before the handler (e.g. a body parser applied to this route).","Retry with a fresh request object rather than reusing a drained stream."],"exampleFix":"// before\nfetch(url, { method: 'PUT', body: JSON.stringify({ data: part }) })\n// after\nfetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/octet-stream' }, body: partBuffer })","handlingStrategy":"validation","validationCode":"if (part.byteLength === 0) {\n  throw new Error('multipart part is empty; refusing to upload')\n}","typeGuard":"function hasBody(body: unknown): body is BodyInit {\n  return body !== undefined && body !== null && !(typeof body === 'string' && body.length === 0)\n}","tryCatchPattern":"const res = await fetch(partUrl, { method: 'PUT', body: partStream })\nif (res.status === 400 && (await res.text()) === 'missing body') {\n  throw new Error('request body was empty or already consumed; resend with raw part bytes')\n}","preventionTips":["Send raw binary part data, not JSON-wrapped payloads.","Never reuse a consumed request/stream object on retry — create a fresh one.","Ensure no middleware (body parser) runs on the part-upload route."],"tags":["http","multipart","body","validation"],"backgroundTag":"missing-request-body","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}