Skip to content

feat(sdk): allow custom request headers (or a fetch override) for header-gated origins #269

Description

@EricAndrechek

Area: sdk — friction (DX) / gap · found via WaveHouse-Stats dogfooding

Expected: point the SDK at a WaveHouse fronted by a header-gated proxy (Cloudflare Access service token, mTLS sidecar, auth gateway) by attaching arbitrary per-request headers — or by supplying a custom fetch.

Actual: ClientConfig exposes only baseURL, auth (which sets just Authorization: Bearer …), and options.maxRetries. No headers option, no fetch injection point — so the SDK can't carry headers like CF-Access-Client-*.

Impact (Stats): initially blocked them (Worker writes went through a CF-Access-gated origin the SDK couldn't satisfy). They've since moved ingest to a public JWT-gated endpoint, so it no longer blocks them — but it still bites any deployment fronting WaveHouse behind a header-gated proxy. A defense-in-depth gate shouldn't force a consumer off the SDK.

Additional consumers named later: cookie-authenticated origins, where the blocker isn't a missing header but that fetch defaults to credentials: "same-origin", so a cross-origin browser request sends no cookie at all.

Related: SDK epic #194; SSE-auth migration #203; base path in baseURL #428 (a deployment behind a BFF generally needs both).


From WaveHouse-Stats WAVEHOUSE-FEEDBACK.md dogfooding (dev 71c2dc4), 2026-06-04.


Scope — all three knobs land together in #456

Rescoped 2026-08-12. #456 already implements the fetch override; headers and fetchOptions are being folded into that same PR so the HTTP-customization surface lands as one coherent API rather than in three increments.

  • options.fetch — supply the HTTP implementation. Exported as FetchLike, deliberately narrower than typeof fetch (a string URL is all the SDK passes), which accepts strictly more implementations than the wide type does. REST path only, by construction. (implemented in feat(sdk)!: add options.headers, fetchOptions, and fetch #456)
  • headers — arbitrary headers on every request. The ergonomic answer for CF-Access and friends; a fetch override is a power tool that shouldn't be the only route to a static header.
  • fetchOptions — a RequestInit merged into every request. Covers credentials: "include" for the cookie case above, plus undici dispatcher, mode, cache, etc., without forcing a full fetch wrapper.

Prior art driving the shape

SDK Options Fetch type
Supabase global.fetch, global.headers type Fetch = typeof fetch
Anthropic fetch, fetchOptions, defaultHeaders, defaultQuery (input: string | URL | Request, init?) => Promise<Response>
OpenAI fetch, fetchOptions same (both Stainless-generated)

fetchOptions is the established name for "extra RequestInit on every request" — OpenAI's own documented undici-proxy example is new OpenAI({ fetch, fetchOptions: { dispatcher: proxyAgent } }). Adopting it is cheaper than inventing a credentials knob that solves one case.

Open design questions

  1. Naming/nesting. Supabase nests under global: {}; Anthropic/OpenAI keep options flat. WaveHouse currently nests transport config under options: {} (maxRetries, fetch). Do headers / fetchOptions sit under options for consistency, or at the top of ClientConfig next to auth?
  2. Static vs dynamic headers. Record<string, string> alone can't carry a rotating service token. A () => Record<string,string> | Promise<…> form can, but inherits the per-call cost question in feat(sdk): define the auth() callback contract — per-request cost, caching, retry staleness #458.
  3. Precedence. When auth, headers, and fetchOptions.headers all set Authorization, which wins? Needs to be defined and tested, not emergent. Anthropic's convention is that per-request options override client defaults, and an explicit null removes a default header.
  4. Whether headers reaches SSE. Native EventSource cannot set headers at all — see below.

Out of scope — split out

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/sdkTypeScript SDK (clients/ts/)breaking-changeBreaking change to public API, CLI, or configenhancementNew feature or request

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions