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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions