Skip to content

fix(functions): merge headers case-insensitively - #2578

Open
Sy-D wants to merge 1 commit into
supabase:masterfrom
Sy-D:fix/functions-header-case
Open

fix(functions): merge headers case-insensitively#2578
Sy-D wants to merge 1 commit into
supabase:masterfrom
Sy-D:fix/functions-header-case

Conversation

@Sy-D

@Sy-D Sy-D commented Jul 29, 2026

Copy link
Copy Markdown

Description

Header names are case-insensitive (RFC 9110), but an object spread only overrides on an exact key match. invoke merged three unnormalized sources, so an entry differing only in case survived alongside its counterpart — and fetch joins two same-name headers into one comma-separated value, so the result is a malformed header rather than a wrong-but-valid one.

new FunctionsClient(url, { headers: { 'content-type': 'application/json' } })
  .invoke('fn', { headers: { 'Content-Type': 'text/plain' }, body: 'x' })
// sent: content-type: application/json, text/plain

That silently contradicts both the precedence comment above the merge (invoke > client > default) and the invoke docstring, which tells callers they can override the SDK-chosen Content-Type.

setAuth had the same root cause and a worse consequence. It assigned headers.Authorization, so a client constructed with a lowercase authorization kept sending the stale token alongside the new one:

const client = new FunctionsClient(url, { headers: { authorization: 'Bearer stale' } })
client.setAuth('fresh')
// sent: authorization: Bearer stale, Bearer fresh

No server accepts that, so setAuth breaks the client for anyone who spelled the header in lowercase.

What changed

Header keys are normalized to lowercase at the two boundaries where headers enter the client — the constructor and each invoke — so the existing spread expresses the documented precedence again. The internally chosen Content-Type defaults are written lowercase for the same reason.

normalizeHeaders mirrors the helper of the same name in storage-js (src/lib/common/headers.ts), including its semantics: last value wins, input not mutated. It is duplicated rather than shared because functions-js has no dependency on storage-js and packages/shared currently holds only tracing — happy to extract it into packages/shared instead if you would prefer that.

The existing case-insensitive hasContentTypeHeader check is untouched; it was already correct.

Testing

New packages/core/functions-js/test/header-case.test.ts — 3 unit tests using a captured fetch, asserting through new Headers(...) so the assertion sees what the platform actually sends.

On master all three fail, with exactly the joined values:

case master this branch
invoke Content-Type over client content-type application/json, text/plain text/plain
setAuth over client authorization Bearer stale, Bearer fresh Bearer fresh
invoke x-custom over client X-Custom client, invoke invoke

Full nx test functions-js (testcontainers-backed Deno relay):

Suites Tests
master 7 passed / 7 42 passed, 1 skipped
this branch 8 passed / 8 45 passed, 1 skipped

Both fully green; the delta is exactly this suite and its 3 tests.

nx lint functions-js reports 17 errors both here and on master — all pre-existing. nx format:check and nx build functions-js pass.

nx test supabase-js fails identically on this branch and on master with The type definition dist/index.d.cts does not exist, which is a local build-order issue unrelated to this change.

Related, not fixed here

The same class of bug exists in auth-js. lib/fetch.ts merges headers with plain spreads and sets headers['Authorization'] by exact case, so a client created with global.headers: { authorization: '…' } (lowercase, a documented option) sends every auth request with authorization: Bearer <jwt>, Bearer <custom>. Reachable straight from createClient.

I have left that out of this PR because auth-js is security-critical and has a wider header surface, so it deserves its own review. Happy to open it separately — or to fold it in here if you would rather have one change.

Type of Change

  • Bug fix (fix)

Checklist

  • Code formatted (nx format)
  • Unit tests added and passing
  • Package suite passing (nx test functions-js, fully green)
  • Builds passing (nx build functions-js)
  • Used conventional commits

Header names are case-insensitive (RFC 9110), but an object spread only
overrides on an exact key match. `invoke` merged three unnormalized
sources, so an entry differing only in case survived alongside its
counterpart and `fetch` joined the two into one comma-separated value:

    new FunctionsClient(url, { headers: { 'content-type': 'application/json' } })
      .invoke('fn', { headers: { 'Content-Type': 'text/plain' }, body: 'x' })
    // sent: content-type: application/json, text/plain

`setAuth` had the same problem and a worse consequence: it assigned
`headers.Authorization`, so a client constructed with a lowercase
`authorization` kept sending the stale token too, as
`authorization: Bearer stale, Bearer fresh`.

Normalize header keys to lowercase at both boundaries — the constructor
and each invoke — so the documented precedence (invoke > client >
default) actually holds. Mirrors `normalizeHeaders` in storage-js.
@Sy-D
Sy-D requested review from a team as code owners July 29, 2026 21:07
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 59838ea4-beec-4876-a1e7-6406c387dcf4

📥 Commits

Reviewing files that changed from the base of the PR and between 6331898 and ae79b4a.

📒 Files selected for processing (3)
  • packages/core/functions-js/src/FunctionsClient.ts
  • packages/core/functions-js/src/helper.ts
  • packages/core/functions-js/test/header-case.test.ts

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved request header handling to be consistently case-insensitive.
    • Ensured invocation-level headers correctly override client-level headers regardless of capitalization.
    • Updated authorization headers reliably when authentication changes.
    • Prevented duplicate header values from being sent in requests.

Walkthrough

normalizeHeaders was added to lowercase header keys, resolve case-insensitive duplicates using the last value, and preserve input objects. FunctionsClient now applies normalization to configured headers, authentication updates, default content types, and invocation-level headers. New tests verify correctly overridden content type, authorization, and custom headers when casing differs.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant