From 38313639ed1c61e8f173eea8abcc57716775d6a4 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:51:12 +0200 Subject: [PATCH] fix: use callable .get to detect Headers in pickRequestHost --- lib/__tests__/domain-branding.test.ts | 10 ++++++++++ lib/admin/domain-branding.ts | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/__tests__/domain-branding.test.ts b/lib/__tests__/domain-branding.test.ts index 6672a2cb..f75414a8 100644 --- a/lib/__tests__/domain-branding.test.ts +++ b/lib/__tests__/domain-branding.test.ts @@ -111,6 +111,16 @@ describe('pickRequestHost', () => { it('lower-cases the result', () => { expect(pickRequestHost(mockHeaders({ host: 'EXAMPLE.com' }))).toBe('example.com'); }); + + it('handles a ReadonlyHeaders-shaped object with an internal `headers` field', () => { + // `await headers()` returns Next's ReadonlyHeaders, which exposes `.get` + // directly but also carries an internal `headers` property. Ensure we use + // its own `.get` rather than descending into `.headers` (#585). + const readonlyLike = Object.assign(mockHeaders({ host: 'ro.example.com' }), { + headers: { notCallable: true }, + }); + expect(pickRequestHost(readonlyLike as unknown as Headers)).toBe('ro.example.com'); + }); }); describe('matchDomainBranding', () => { diff --git a/lib/admin/domain-branding.ts b/lib/admin/domain-branding.ts index 61de464a..fe6a9dd3 100644 --- a/lib/admin/domain-branding.ts +++ b/lib/admin/domain-branding.ts @@ -113,7 +113,15 @@ type HeadersLike = Headers | { get(name: string): string | null }; * host header is set. */ export function pickRequestHost(headersOrReq: NextRequest | HeadersLike): string | null { - const headers: HeadersLike = 'headers' in headersOrReq ? (headersOrReq as NextRequest).headers : headersOrReq; + // A Headers / ReadonlyHeaders exposes `.get` directly; a NextRequest carries + // its headers under `.headers`. Discriminate on the callable `.get` rather + // than the presence of a `headers` property, since ReadonlyHeaders (returned + // by `await headers()`) also has an internal `headers` field (#585). + const candidate = headersOrReq as { get?: unknown }; + const headers: HeadersLike = + typeof candidate.get === 'function' + ? (headersOrReq as HeadersLike) + : (headersOrReq as NextRequest).headers; const raw = headers.get('x-forwarded-host') || headers.get('host'); if (!raw) return null; const first = raw.split(',')[0]?.trim();