[PR #464] [CLOSED] fix: return empty headers/cookies when called outside request context #590

Closed
opened 2026-05-06 13:08:56 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/464
Author: @benfavre
Created: 3/11/2026
Status: Closed

Base: mainHead: fix/headers-cookies-graceful-fallback


📝 Commits (1)

  • 5d85f6f fix: return empty headers/cookies when called outside request context

📊 Changes

1 file changed (+11 additions, -11 deletions)

View changed files

📝 packages/vinext/src/shims/headers.ts (+11 -11)

📄 Description

Problem

When headers() or cookies() is called outside of a request's AsyncLocalStorage scope, the current code returns a rejected Promise. This crashes the process because:

  1. Libraries like TRPC call headers() inside React.cache() wrappers at module initialization time — before runWithHeadersContext() sets up the ALS scope
  2. The rejected Promise propagates as an unhandled rejection
  3. The process crashes before any request is served

Stack trace

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.

This happens during RSC module evaluation, not from a Client Component.

Solution

Return empty readonly Headers / RequestCookies objects instead of rejecting when state.headersContext is null.

This matches Next.js behavior — their headers() and cookies() gracefully degrade when called outside request context. The real request headers become available when the component or procedure is actually invoked during rendering within the proper ALS scope.

Changes

packages/vinext/src/shims/headers.ts

  • headers(): Return _sealHeaders(new Headers()) wrapped in _decorateRequestApiPromise instead of _decorateRejectedRequestApiPromise
  • cookies(): Return _sealCookies(new RequestCookies(new Map())) wrapped in _decorateRequestApiPromise instead of _decorateRejectedRequestApiPromise

Both return readonly wrappers (sealed via Proxy) so mutation attempts still throw.

Reproduction

// In a TRPC router (RSC module scope):
import { headers } from "next/headers";

const getHeaders = React.cache(() => headers());

// This runs at module evaluation time, before any request context exists.
// Current: unhandled rejection → crash
// Fixed: returns empty Headers, real headers available at call time

Risk

Low. The empty objects are readonly (mutations throw), and the fix only affects the edge case where there is no ALS store. Normal request-scoped calls are unaffected — the code path for state.headersContext !== null is unchanged.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/464 **Author:** [@benfavre](https://github.com/benfavre) **Created:** 3/11/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/headers-cookies-graceful-fallback` --- ### 📝 Commits (1) - [`5d85f6f`](https://github.com/cloudflare/vinext/commit/5d85f6f8de10cb04f63678d5ce76011a8455dfff) fix: return empty headers/cookies when called outside request context ### 📊 Changes **1 file changed** (+11 additions, -11 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/shims/headers.ts` (+11 -11) </details> ### 📄 Description ## Problem When `headers()` or `cookies()` is called outside of a request's AsyncLocalStorage scope, the current code returns a rejected Promise. This crashes the process because: 1. Libraries like [TRPC](https://trpc.io/) call `headers()` inside `React.cache()` wrappers at **module initialization time** — before `runWithHeadersContext()` sets up the ALS scope 2. The rejected Promise propagates as an unhandled rejection 3. The process crashes before any request is served ### Stack trace ``` 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. ``` This happens during RSC module evaluation, not from a Client Component. ## Solution Return empty readonly `Headers` / `RequestCookies` objects instead of rejecting when `state.headersContext` is null. This matches Next.js behavior — their `headers()` and `cookies()` gracefully degrade when called outside request context. The real request headers become available when the component or procedure is actually invoked during rendering within the proper ALS scope. ## Changes **`packages/vinext/src/shims/headers.ts`** - `headers()`: Return `_sealHeaders(new Headers())` wrapped in `_decorateRequestApiPromise` instead of `_decorateRejectedRequestApiPromise` - `cookies()`: Return `_sealCookies(new RequestCookies(new Map()))` wrapped in `_decorateRequestApiPromise` instead of `_decorateRejectedRequestApiPromise` Both return readonly wrappers (sealed via Proxy) so mutation attempts still throw. ## Reproduction ```typescript // In a TRPC router (RSC module scope): import { headers } from "next/headers"; const getHeaders = React.cache(() => headers()); // This runs at module evaluation time, before any request context exists. // Current: unhandled rejection → crash // Fixed: returns empty Headers, real headers available at call time ``` ## Risk Low. The empty objects are readonly (mutations throw), and the fix only affects the edge case where there is no ALS store. Normal request-scoped calls are unaffected — the code path for `state.headersContext !== null` is unchanged. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:08:56 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#590
No description provided.