Skip to content

fetch transport: permanent-error classification; both transports: 204/304/HEAD rows, CONTROL_BYTE regex, producer-failure race #82

Description

@Wahbeh-Mohammad

Part of the audit remediation umbrella #67. Milestone 5. Severity: MEDIUM. Requirements: TRANSPORT-9, TRANSPORT-14, TRANSPORT-20, RETRY-2, SEAM-30.

Current SDK behavior

  • Every native fetch error is retryable. fetch-transport.ts:282-289 wraps every non-abort
    error as TransportFailureError, which classify.ts:71 treats as retryable. Request
    accepts any absolute URL, so ftp://example.com reaches fetch, which rejects with a
    permanent TypeError; the retry engine spends the whole budget on it. The undici transport
    has a TERMINAL_ARGUMENT_CODES guard (undici-transport.ts:185-231) with a comment stating
    why a permanent misconfiguration must not retry. The two transports classify the same
    condition oppositely.
  • No body-less response coverage. For 204, 304 and any HEAD response, fetch forwards
    globalThis.Response.body, which is null (fetch-transport.ts:182); undici wraps
    result.body unconditionally in toDemandDrivenStream (undici-transport.ts:349), a
    non-null empty stream. fetch sets reasonPhrase (:180); undici never does. No fixture in
    packages/transport-conformance/src/fixtures.ts:24-111 returns 204/304 and no row issues
    HEAD.
  • CONTROL_BYTE omits LF. transport-shared/src/header-mapping.ts:6 is
    /[\x00-\x08\x0B-\x1F\x7F]/u, skipping \x0A along with the intended \x09. Harmless
    today because Headers.addInbound rejects LF and the try/catch drops the header.
  • Producer-failure race. When producerFailure wins the race
    (fetch-transport.ts:278-281, undici-transport.ts:489-504), send() rejects while the
    native call is pending. abandon unwinds the producer but does not abort the fork, so a
    response that arrives later is discarded with its body neither cancelled nor drained.
    sdk-design-nodejs/03-….md:86-90 states the obligation. Not driven by the auditor.

Expected behavior

Both transports throw a non-retryable error for a permanent misconfiguration and a retryable
TransportFailureError for a network failure. Both transports produce the same
Response.body shape for 204, 304 and HEAD, and the reasonPhrase gap is ledgered beside
§10 item 13. The inbound control-byte gate covers every C0 byte except HTAB. A send that fails
on the producer side aborts the native call so no response is stranded.

Notes and leads

  • fetch: map TypeErrors for unsupported scheme, invalid URL, forbidden method, and invalid
    header to a non-IoError DexpaceError (or the bare TypeError undici uses). Keep
    fetch failed with a network cause as TransportFailureError.
  • Body-less: decide body === null for body-less responses (the WHATWG shape), make undici
    match, and add conformance rows for 204, 304 and HEAD asserting body, contentLength,
    reasonPhrase.
  • Regex: /[\x00-\x08\x0A-\x1F\x7F]/u.
  • Race: abort the forked signal in the producer-failure branch, then verify with an
    instrumented transport whose native call resolves after the producer fails.
  • Add a conformance row for an unsupported scheme that both transports run.
  • Patch changesets for @dexpace/transport-fetch, @dexpace/transport-undici,
    @dexpace/transport-shared.

Activity

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

Metadata

Metadata

Labels

area:transportTransport, async model, seams: TRANSPORT-* ASYNC-* SEAM-*spec:conformanceConformance-suite behavior, or a gap in the suite itselftype:bugDefect in shipped behavior

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions