From 5d85f6f8de10cb04f63678d5ce76011a8455dfff Mon Sep 17 00:00:00 2001 From: Benjamin Favre Date: Wed, 11 Mar 2026 08:57:21 +0000 Subject: [PATCH] fix: return empty headers/cookies when called outside request context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `headers()` or `cookies()` is called outside of a request's AsyncLocalStorage scope (e.g. during RSC module initialization, inside React `cache()` wrappers, or at import-time in libraries like TRPC), the current code rejects with an error. This crashes the process because the rejected promise propagates as an unhandled rejection. Instead, return empty readonly `Headers` / `RequestCookies` objects. This matches Next.js behavior where these APIs gracefully degrade outside request context. The real request headers become available when the component or procedure is actually invoked during rendering within the proper ALS scope. Reproduction: 1. Use a library (e.g. TRPC) that calls `headers()` inside `React.cache()` at module initialization time 2. The ALS store is not yet set up (no `runWithHeadersContext` scope) 3. `headers()` rejects → unhandled rejection → process crash With this fix, step 3 returns empty Headers instead. --- packages/vinext/src/shims/headers.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/vinext/src/shims/headers.ts b/packages/vinext/src/shims/headers.ts index 055cc2b08..8e7518db0 100644 --- a/packages/vinext/src/shims/headers.ts +++ b/packages/vinext/src/shims/headers.ts @@ -513,12 +513,14 @@ export function headers(): Promise & Headers { const state = _getState(); if (!state.headersContext) { - return _decorateRejectedRequestApiPromise( - new Error( - "headers() can only be called from a Server Component, Route Handler, " + - "or Server Action. Make sure you're not calling it from a Client Component.", - ), - ); + // Return empty readonly headers instead of rejecting. + // Libraries like TRPC call headers() inside React cache() during RSC + // module initialization, before runWithHeadersContext sets up the ALS + // scope. Rejecting here crashes the process; returning empty headers + // lets initialization proceed. The real request headers are available + // when the TRPC procedure is actually invoked during rendering. + const emptyHeaders = _sealHeaders(new Headers()); + return _decorateRequestApiPromise(Promise.resolve(emptyHeaders), emptyHeaders); } if (state.headersContext.accessError) { @@ -543,11 +545,9 @@ export function cookies(): Promise & RequestCookies { const state = _getState(); if (!state.headersContext) { - return _decorateRejectedRequestApiPromise( - new Error( - "cookies() can only be called from a Server Component, Route Handler, or Server Action.", - ), - ); + // Return empty readonly cookies instead of rejecting (same rationale as headers()). + const emptyCookies = _sealCookies(new RequestCookies(new Map())); + return _decorateRequestApiPromise(Promise.resolve(emptyCookies), emptyCookies); } if (state.headersContext.accessError) {