Skip to content

fix(sdk): treat a stream 429 as retryable and honor Retry-After #469

Description

@EricAndrechek

Area: sdk — streaming reliability. Surfaced in review of #203, deliberately not changed there.

The fetch-based stream transport treats any 4xx as terminal: parseErrorResponse (clients/ts/src/errors.ts) marks only >= 500 retryable, and _attempt ends the stream on a non-retryable status. That rule is right for the cases it was written for — a rejected token or a missing table will not be talked round by repeating the request — and it matches the REST path.

429 Too Many Requests is the exception. It is explicitly a retry-later status, usually carrying Retry-After. WaveHouse itself never emits it on /v1/stream (the endpoint is ungated), so it comes from a fronting proxy, CDN, or API gateway — exactly the deployments this transport is meant to work behind.

The failure mode is unpleasant: a brief upstream blip drops many streams at once, they all reconnect, the proxy rate-limits the burst, and every client takes a terminal HTTP_429 and stays dead until the page reloads or the process restarts. The jittered backoff added in #203 exists to avoid exactly that thundering herd, and a terminal 429 defeats it at the last step.

Proposal

Treat 429 as retryable on the stream path, and honor Retry-After as a backoff floor — the mechanism already exists (_retryFloorMs, fed today only by an SSE retry: field, clamped to MAX_BACKOFF_MS).

  • Parse Retry-After in both forms: delta-seconds and an HTTP-date.
  • Clamp as retry: already is, so a hostile or fat-fingered value can't strand a stream.
  • Keep every other 4xx terminal.

The REST path already special-cases 503 + Retry-After (clients/ts/src/http.ts), so this is consistent with how the SDK treats the other "come back later" status rather than a new concept.

Worth deciding explicitly

Whether REST should get the same treatment for 429. Today a REST 429 is non-retryable too. Arguably the same argument applies, but the blast radius differs: a REST call returns an error the caller can act on, whereas a dead stream is silent until someone notices data stopped arriving.

Acceptance

  • 429 on a stream is retryable and re-dials
  • Retry-After (seconds and HTTP-date) sets the backoff floor, clamped to 30s
  • Every other 4xx stays terminal — pinned by a test, since this is the rule protecting against retrying a rejected token forever
  • Docs: the error table in docs/src/content/docs/sdk/reference.md and the "4xx is terminal" prose in sdk/streaming.md

Related: #203 (introduced the terminal-4xx rule and the backoff), #465 (the other streaming-load issue).

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/sdkTypeScript SDK (clients/ts/)area/streamingSSE / live-query delivery path (/v1/stream)enhancementNew feature or request

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions