Skip to content

STREAM_READ_EXPOSED_HEADERS

const STREAM_READ_EXPOSED_HEADERS: readonly string[]

Defined in: packages/server/src/circuits/edge.ts:214

The response headers a browser must be allowed to READ off a stream-edge response.

CORS lets script see only a short safelist of response headers (cache-control, content-language, content-length, content-type, expires, last-modified, pragma). Every header the durable-streams protocol answers with is outside it, so a CROSS-ORIGIN reader sees NONE of them unless the mount names them on Access-Control-Expose-Headers.

Two generations of client read these. @pgxsinkit/client’s own reader (READ_RESPONSE_HEADERS in its circuits/long-poll.ts, which a unit test holds to this list) steers by Stream-Next-Offset, Stream-Up-To-Date (a presence check, which is why it is easy to miss), Stream-Cursor and Stream-Closed. Stripped of them it cannot make progress and fails the read, loudly. Clients still in the field on @durable-streams/client (0.2.x, which pgxsinkit read through until ADR-0065 decision 6) drive their whole read loop off the same four and also read stream-sse-data-encoding (SSE payload decoding) and etag (the stream-metadata path). Stripped of them, that client never learns an offset: it re-requests offset=-1 forever and never switches to a live long poll, which presents as a hot loop of hundreds of requests per second per shape with no error raised anywhere. Both floor their retry backoff on Retry-After; without it they fall back on their own schedule.

Stream-Seq, Stream-TTL and Stream-Expires-At are read by neither. They are named here anyway because this list is a statement about the ds protocol’s response namespace rather than about which subset one client version happens to read today — exposing a header a response never carries is inert, while omitting one that it does carry wedges the reader. The Producer-* headers are deliberately absent: they belong to the write path, which does not come through this gate.

EVERY mount of createStreamGate must put these on Access-Control-Expose-Headers of the ACTUAL (non-preflight) response. The gate cannot do it itself — it forwards the upstream response’s headers (verbatim for an ordinary shape, minus the caching ones for a rewritten one), and an exposure list is meaningless without the Access-Control-Allow-Origin decision that only the mount owns.