Repository navigation
Body lifecycle: zero-length chunks in streamBody, multipart boundary quoting, withResponseLogging after close() #77
Copy link
Copy link
Closed
Labels
area:coreCore HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*type:bugDefect in shipped behaviorDefect in shipped behavior
Milestone
Description
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, checksdone, checks over-run, then writes. There is novalue.length === 0guard.
A source that enqueuesnew 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-71documents that a zero-length chunk is HTTP/1.1 chunked encoding's
terminating chunk.io/retention-window.ts:177-183andbody/response-body-logging.ts:82-88
enforce the rule that this path omits. - Multipart boundary is not quoted.
multipart-body.ts:160builds
multipart/form-data; boundary=${boundary}.BOUNDARY_PATTERN(:29-31) admits RFC 2046
bchars, which include space,,,:,=,?,/,(,). Those are nottchar.
multipartBody(parts, 'a,b')yields a header Node's ownFormDataparser rejects with
TypeError: Failed to parse body as FormData. snapshot()afterclose()poisonserror().withResponseLogging'ssnapshot()
(response-body-logging.ts:241) callsstartDrainunconditionally.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, anderror()(:244) returns it forever.read()afterclose()rejects with the
same rawTypeErrorinstead ofClosedResourceError.- 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,withResponseLogginghave 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 asretention-window.ts. Test an empty-only source and an empty chunk between real
chunks. - Boundary: quote when the boundary is not a pure
tchartoken (boundary="a,b"), reusing
MediaType's parameter quoting. Or narrowvalidateBoundarytotcharand record the
narrowing. Round-trip test through Node'sResponse.formData(). - Logging tap: check
state.closedat the top ofstartDrain,snapshot, andread.
Tests: close-then-snapshot, close-then-read, close-then-error. - Node conformance: add one
*.test.mjsper bridge undertests/node-conformance/. Cases
belong there, not only inbun run test(CLAUDE.md, runtime-divergent surface). - Patch changeset for
@dexpace/core.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:coreCore HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*type:bugDefect in shipped behaviorDefect in shipped behavior
Activity
Wahbeh-Mohammad commentedon Sep 5, 2026
Addressed in PR #91, merged into
audit/remediation-67; reachesmvpthrough PR #98. Decision record:docs/audit-67-decisions.md.