Skip to content

Body lifecycle: zero-length chunks in streamBody, multipart boundary quoting, withResponseLogging after close() #77

Description

@Wahbeh-Mohammad

Part of the audit remediation umbrella #67. Milestone 4. Severity: HIGH (first item), MEDIUM (rest). Requirements: BODY-10, HTTP-39, HTTP-51, BODY-26, BODY-28, IO-16.

Current SDK behavior

  • Empty chunks reach the sink. #writeExactly (packages/core/src/body/stream-body.ts:79-91)
    reads, checks done, checks over-run, then writes. There is no value.length === 0 guard.
    A source that enqueues new Uint8Array(0) is never diagnosed; the auditor's probe forwarded
    200,000 empty chunks before the source closed, then reported
    EndOfStreamError: delivered 0 of 3 bytes. Each empty chunk reaches the transport sink.
    io/buffered-sink.ts:66-71 documents that a zero-length chunk is HTTP/1.1 chunked encoding's
    terminating chunk. io/retention-window.ts:177-183 and body/response-body-logging.ts:82-88
    enforce the rule that this path omits.
  • Multipart boundary is not quoted. multipart-body.ts:160 builds
    multipart/form-data; boundary=${boundary}. BOUNDARY_PATTERN (:29-31) admits RFC 2046
    bchars, which include space, ,, :, =, ?, /, (, ). Those are not tchar.
    multipartBody(parts, 'a,b') yields a header Node's own FormData parser rejects with
    TypeError: Failed to parse body as FormData.
  • snapshot() after close() poisons error(). withResponseLogging's snapshot()
    (response-body-logging.ts:241) calls startDrain unconditionally. closeDelegate (:62)
    already released the reader, so the drain throws
    TypeError: Invalid state: The reader is not attached to a stream. The catch at :119-122
    caches it, and error() (:244) returns it forever. read() after close() rejects with the
    same raw TypeError instead of ClosedResourceError.
  • No Node-conformance coverage for the stream bridges. toReadableStream
    (io/buffered-source.ts:306), toWritableStream (io/buffered-sink.ts:127),
    io/tee-sink.ts:138, withRequestLogging, withResponseLogging have zero cases under
    tests/node-conformance/.

Expected behavior

streamBody treats a zero-length read for a positive request as a stream-contract violation
and writes no empty chunk to the sink. The multipart Content-Type a peer receives parses
with any RFC 9110 parameter parser. After close(), snapshot() returns the captured prefix
without starting a drain, read() rejects with ClosedResourceError, and error() reports
only a real drain failure. The pull, cancel, and lock behavior of the Web Streams bridges is
proven on Node as well as on Bun.

Notes and leads

  • Empty chunks: if (value.length === 0) throw new SourceContractViolationError(...), same
    wording as retention-window.ts. Test an empty-only source and an empty chunk between real
    chunks.
  • Boundary: quote when the boundary is not a pure tchar token (boundary="a,b"), reusing
    MediaType's parameter quoting. Or narrow validateBoundary to tchar and record the
    narrowing. Round-trip test through Node's Response.formData().
  • Logging tap: check state.closed at the top of startDrain, snapshot, and read.
    Tests: close-then-snapshot, close-then-read, close-then-error.
  • Node conformance: add one *.test.mjs per bridge under tests/node-conformance/. Cases
    belong there, not only in bun run test (CLAUDE.md, runtime-divergent surface).
  • Patch changeset for @dexpace/core.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions