Skip to content

Serde and pagination: deserializeFrom abort, paginator leak on malformed PageInfo, tristate null cast, SSE double-reported release failure #79

Description

@Wahbeh-Mohammad

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.

Activity

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

Metadata

Metadata

Labels

area:coreCore HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*area:serdeSerialization and deserialization: SERDE-*area:streamingServer-sent events and pagination: SSE-* PAGE-*type:bugDefect in shipped behavior

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions