Skip to content

feat(sdk): per-call headers, fetchOptions and fetch overrides, plus dynamic header callbacks #459

Description

@EricAndrechek

Area: sdk — DX · follow-up to #456 / #269

#456 adds HTTP customization at the client level: options.fetch, options.headers (static), and options.fetchOptions. This issue covers the per-call half, plus dynamic header callbacks, both deliberately deferred to keep #456 reviewable.

Scope

  • Per-call headers — override or add headers for one query
  • Per-call fetchOptions — e.g. Next.js next: { tags } cache control on a specific query (supabase-js#917 is people reaching for a whole custom fetch just for this)
  • Per-call fetch — the framework-scoped case: SvelteKit's load() hands you a request-scoped fetch with cookie access that can hit internal endpoints without a real HTTP round trip. Open at Supabase since 2022 (supabase-js#438, #889)
  • Dynamic headers() => Record<string,string> | Promise<Record<string,string>> alongside the static form, for rotating credentials

Why this is cheap

The per-call slot already exists — .fetch(opts?: RequestOptions) on QueryBuilder (×2), TableRef, and PipeRef. Adding optional fields to that interface is purely additive; nothing breaks.

The expensive part ships in #456 regardless: case-insensitive header merging and the precedence rules. #456 keeps that in a single helper called from request(), so the work here is threading three optional fields to one existing merge site and testing precedence.

Per-call fetch is the smallest of the three: request() reads ctx.options.fetch today; per-call makes it opts.fetch ?? ctx.options.fetch.

Precedence

#456 establishes, lowest to highest: client headersauth → SDK-computed (Content-Type, Accept). Per-call headers slot in at the top — an explicit per-call header is the caller overriding deliberately. That ordering needs tests here, not just documentation; both known bugs in this area at Supabase were merge bugs, not design bugs (#2207 — a global Content-Type got joined with an upload's own, producing application/json, image/png and 415s; #1043 — a case-sensitive check against case-insensitive headers).

Dependency on #458

The dynamic-header callback raises exactly the question #458 asks about auth: it's invoked per request with no caching or in-flight dedupe. Both should be answered together rather than each growing its own convention — settle #458 first, then apply the same contract to header callbacks.

Static headers ship in #456 because they cover the motivating case: CF-Access service tokens are long-lived ID/secret pairs from env vars, not per-request-minted credentials.

Known ergonomic wrinkle

The query builder is PromiseLikethen() calls .fetch() with no arguments — so per-call options require the explicit terminal form:

await wh.from('clicks').select('*');                      // no per-call options possible
await wh.from('clicks').select('*').fetch({ headers });   // per-call options

Already true for signal today, so not a regression. Making await carry options would need a chainable .with({ … }), which is a larger API decision and explicitly not proposed here.

Not in scope

Streaming — no per-call or client-level header reaches SSE while the transport is EventSource. Tracked in #203.

— Filed by Claude Opus 5, via Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/sdkTypeScript SDK (clients/ts/)enhancementNew feature or request

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions