hcengineering/platform · warning

missing body

Error message

missing body

What it means

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.

Solutions

  1. Send the raw part bytes as the request body (do not wrap in JSON) and ensure the stream is not pre-consumed.
  2. Check no middleware reads req before the handler (e.g. a body parser applied to this route).
  3. Retry with a fresh request object rather than reusing a drained stream.

Example fix

// before
fetch(url, { method: 'PUT', body: JSON.stringify({ data: part }) })
// after
fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/octet-stream' }, body: partBuffer })
Defensive patterns

Strategy: validation

Validate before calling

if (part.byteLength === 0) {
  throw new Error('multipart part is empty; refusing to upload')
}

Type guard

function hasBody(body: unknown): body is BodyInit {
  return body !== undefined && body !== null && !(typeof body === 'string' && body.length === 0)
}

Try / catch

const res = await fetch(partUrl, { method: 'PUT', body: partStream })
if (res.status === 400 && (await res.text()) === 'missing body') {
  throw new Error('request body was empty or already consumed; resend with raw part bytes')
}

Prevention

When it happens

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

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

Related errors


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

Appendix: source

Thrown at services/datalake/pod-datalake/src/handlers/multipart.ts:81

  res: Response,
  datalake: Datalake
): Promise<void> {
  const workspace = req.params.workspace as WorkspaceUuid
  const partNumber = req.query.partNumber as string

  let uuid: string
  let uploadId: string

  const upload = req.query.uploadId as string
  try {
    ;({ uuid, uploadId } = parseUploadId(upload))
  } catch (err: any) {
    res.status(400).send('invalid upload id')
    return
  }

  if (!req.readable) {
    res.status(400).send('missing body')
    return
  }

  const { bucket } = await datalake.selectStorage(ctx, workspace)

  const part = await bucket.uploadMultipartPart(ctx, uuid, { uploadId }, req, {
    size: Number.parseInt(req.headers['content-length'] ?? '0'),
    partNumber: Number.parseInt(partNumber)
  })

  ctx.info('multipart-part', { workspace, uuid, uploadId, partNumber })

  res.status(200).json(part)
}

export async function handleMultipartUploadComplete (
  ctx: MeasureContext,
  req: Request,

View on GitHub (pinned to 63e28dc964)