Part of the audit remediation umbrella #67. Milestone 4. Severity: MEDIUM. Requirements: SERDE-3, SERDE-14, PAGE-2, PAGE-27, SSE-30.
Current SDK behavior
{signal} cannot interrupt a pending read. deserializeFrom
(packages/codec-json/src/json-serde.ts:219-225) checks the signal before the lock and after
each chunk. An await reader.read() that never resolves is not raced against the signal. A
stream that enqueues {"a": and stalls, with a signal aborted 20 ms later, never settles and
leaves source.locked === true. seams/serde.ts:210-212 and
docs/sdk-documentation/write-a-serde.md:118 promise "an aborted call never leaves the
caller's source locked". serializeTo (serde.ts:98) has the same shape. The only tests use
already-aborted signals; tests/node-conformance/serde.test.mjs has no {signal} or
admitsNull case.
- A malformed
PageInfo leaks the response. paginator.ts:180 clears held before
transport.send. If parse resolves to undefined or {items: null}, the invariant at
:203 or the Page constructor at :212 throws while held === undefined, so the finally
at :221 closes nothing. parseOrClose (:231-250) covers parse-throws only. Both
invariant messages (paginator.ts:207-210, page.ts:74-77) say "never null" but test
!== undefined; items: null then throws a bare TypeError from the spread at page.ts:81.
tristate() can produce a present null. tristate-schema.ts:45 does
present(inner.parse(input) as NonNullable<T>). An inner schema that normalizes to null
yields {kind: 'present', value: null}, the fourth state SERDE-14 forbids.
- A release failure during an in-flight SSE error is reported twice.
sse/stream.ts:135,160-176:
the close error is suppressed onto the thrown SuppressedError and also passed to
onReleaseFailure. SSE-30 scopes the hook to the automatic clean-terminal path with no error
in flight.
Expected behavior
An aborted deserializeFrom or serializeTo settles promptly and leaves the caller's stream
unlocked. The paginator closes the response on every exit path, including a strategy that
returns a malformed result, and names the invariant it violated. tristate() never returns a
present null. A release failure is reported once: on the thrown error when an error is in
flight, through onReleaseFailure otherwise.
Notes and leads
- Abort: race
reader.read() against the signal (Promise.race with an abort listener, removed
in finally), then releaseLock() as today. If the race is not wanted, narrow the doc to
"checked between chunks" instead. Mid-drain test in json-serde.test.ts and in
tests/node-conformance/serde.test.mjs.
- Paginator: wrap the two invariants in the same close-then-rethrow discipline as
parseOrClose; test == null to catch both. Tests: parse → undefined, parse → {items: null},
assert close was called once.
tristate: runtime check after inner.parse; throw DeserializationError.
- SSE: skip the hook in
#releaseWithInFlightError.
- Patch changesets for
@dexpace/core and @dexpace/codec-json.
Part of the audit remediation umbrella #67. Milestone 4. Severity: MEDIUM. Requirements: SERDE-3, SERDE-14, PAGE-2, PAGE-27, SSE-30.
Current SDK behavior
{signal}cannot interrupt a pending read.deserializeFrom(
packages/codec-json/src/json-serde.ts:219-225) checks the signal before the lock and aftereach chunk. An
await reader.read()that never resolves is not raced against the signal. Astream that enqueues
{"a":and stalls, with a signal aborted 20 ms later, never settles andleaves
source.locked === true.seams/serde.ts:210-212anddocs/sdk-documentation/write-a-serde.md:118promise "an aborted call never leaves thecaller's source locked".
serializeTo(serde.ts:98) has the same shape. The only tests usealready-aborted signals;
tests/node-conformance/serde.test.mjshas no{signal}oradmitsNullcase.PageInfoleaks the response.paginator.ts:180clearsheldbeforetransport.send. Ifparseresolves toundefinedor{items: null}, the invariant at:203or thePageconstructor at:212throws whileheld === undefined, so thefinallyat
:221closes nothing.parseOrClose(:231-250) covers parse-throws only. Bothinvariant messages (
paginator.ts:207-210,page.ts:74-77) say "never null" but test!== undefined;items: nullthen throws a bareTypeErrorfrom the spread atpage.ts:81.tristate()can produce a presentnull.tristate-schema.ts:45doespresent(inner.parse(input) as NonNullable<T>). An inner schema that normalizes tonullyields
{kind: 'present', value: null}, the fourth state SERDE-14 forbids.sse/stream.ts:135,160-176:the close error is suppressed onto the thrown
SuppressedErrorand also passed toonReleaseFailure. SSE-30 scopes the hook to the automatic clean-terminal path with no errorin flight.
Expected behavior
An aborted
deserializeFromorserializeTosettles promptly and leaves the caller's streamunlocked. The paginator closes the response on every exit path, including a strategy that
returns a malformed result, and names the invariant it violated.
tristate()never returns apresent
null. A release failure is reported once: on the thrown error when an error is inflight, through
onReleaseFailureotherwise.Notes and leads
reader.read()against the signal (Promise.racewith an abort listener, removedin
finally), thenreleaseLock()as today. If the race is not wanted, narrow the doc to"checked between chunks" instead. Mid-drain test in
json-serde.test.tsand intests/node-conformance/serde.test.mjs.parseOrClose; test== nullto catch both. Tests:parse → undefined,parse → {items: null},assert
closewas called once.tristate: runtime check afterinner.parse; throwDeserializationError.#releaseWithInFlightError.@dexpace/coreand@dexpace/codec-json.