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.
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
#writeExactly(packages/core/src/body/stream-body.ts:79-91)reads, checks
done, checks over-run, then writes. There is novalue.length === 0guard.A source that enqueues
new Uint8Array(0)is never diagnosed; the auditor's probe forwarded200,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'sterminating chunk.
io/retention-window.ts:177-183andbody/response-body-logging.ts:82-88enforce the rule that this path omits.
multipart-body.ts:160buildsmultipart/form-data; boundary=${boundary}.BOUNDARY_PATTERN(:29-31) admits RFC 2046bchars, which include space,,,:,=,?,/,(,). Those are nottchar.multipartBody(parts, 'a,b')yields a header Node's ownFormDataparser rejects withTypeError: 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-122caches it, and
error()(:244) returns it forever.read()afterclose()rejects with thesame raw
TypeErrorinstead ofClosedResourceError.toReadableStream(
io/buffered-source.ts:306),toWritableStream(io/buffered-sink.ts:127),io/tee-sink.ts:138,withRequestLogging,withResponseLogginghave zero cases undertests/node-conformance/.Expected behavior
streamBodytreats a zero-length read for a positive request as a stream-contract violationand writes no empty chunk to the sink. The multipart
Content-Typea peer receives parseswith any RFC 9110 parameter parser. After
close(),snapshot()returns the captured prefixwithout starting a drain,
read()rejects withClosedResourceError, anderror()reportsonly 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
if (value.length === 0) throw new SourceContractViolationError(...), samewording as
retention-window.ts. Test an empty-only source and an empty chunk between realchunks.
tchartoken (boundary="a,b"), reusingMediaType's parameter quoting. Or narrowvalidateBoundarytotcharand record thenarrowing. Round-trip test through Node's
Response.formData().state.closedat the top ofstartDrain,snapshot, andread.Tests: close-then-snapshot, close-then-read, close-then-error.
*.test.mjsper bridge undertests/node-conformance/. Casesbelong there, not only in
bun run test(CLAUDE.md, runtime-divergent surface).@dexpace/core.