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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions