* refactor: consolidate 5 nested ALS scopes into unified request context
Every App Router request previously ran through 5-6 nested
AsyncLocalStorage.run() calls (headers, navigation, cache-state,
private-cache, fetch-cache, plus optional execution-context). Each
ALS scope push/pop has measurable cost on Workers isolates.
Introduce a single UnifiedRequestContext that holds all per-request
state in one flat object, backed by one ALS instance. Each shim module
checks isInsideUnifiedScope() first and reads its sub-fields from the
unified store, falling back to its own standalone ALS when outside
(SSR environment, Pages Router, tests).
- Create unified-request-context.ts with createRequestContext(),
runWithRequestContext(), getRequestContext(), isInsideUnifiedScope()
- Rewire _getState() in headers, navigation-state, cache, cache-runtime,
fetch-cache, and request-context to dual-path (unified first, own ALS
fallback)
- Make each runWith*() a no-op inside unified scope
- Replace 5-deep nesting in app-rsc-entry.ts (main handler + ISR regen)
with single _runWithUnifiedCtx() call
- Export ensureFetchPatch() from fetch-cache for standalone patch install
- Add 17 unit tests for unified context (isolation, nesting, concurrency)
* test: update entry-templates snapshots for unified ALS context
Snapshots were stale after the unified request context refactor changed
the generated entry code (new imports, removed 5-deep nesting).
* test: update app router waitUntil assertion
* refactor: unify request ALS across router flows
* fix: address unified request context review feedback
* fix: address unified ALS review feedback
* fix: address remaining unified ALS review feedback
* fix: wrap Pages Router ISR regeneration in unified context
Ensures patched fetch and ALS-based context are available during
background regeneration, matching App Router behavior.
- Creates fresh unified context with execution context for regen
- Calls ensureFetchPatch() to enable cache tagging
- Adds test verifying _runWithUnifiedCtx and ensureFetchPatch presence
- Updates snapshot
Part of Phase 1.5: minimal Pages Router fix for ISR parity.
* Fix dev ISR unified context parity
* fix: address unified ALS review followups
* Fix pages router unified execution context seeding
* Fix Pages Router ISR rerender state isolation
* fix: address unified ALS review feedback
* fix: dedup stale background refetches in fetch cache
Every concurrent request hitting a stale cache entry was independently
firing its own originalFetch(), creating a thundering herd on popular
endpoints. Add a per-cache-key dedup map (pendingRefetches) shared
across RSC/SSR environments via Symbol.for(), matching the existing
ISR regeneration dedup pattern in isr-cache.ts.
Also adds a 60s timeout safety net that force-cleans dedup entries
when upstream fetches hang, with identity checks in both .finally()
and setTimeout to prevent slot-ownership races.
* test: add fetch cache stale dedup concurrency tests
- Concurrent stale hits trigger only one background refetch
- Completed refetches allow new ones (lifecycle test)
- Failed refetches clean up dedup entry (error-path test)
- Hung fetches are force-cleaned after timeout
- Late-settling hung fetch does not evict replacement refetch
- _resetPendingRefetches() called in beforeEach for test isolation
* Update packages/vinext/src/shims/fetch-cache.ts
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
* fix: repair broken setTimeout structure from review suggestion
The applied suggestion duplicated the setTimeout block and broke
brace nesting. Fix the structure and also clear the timeout in
.finally() so completed refetches don't leave dangling timers.
* fix: wrap fake timer tests in try/finally for safety
If a test throws before vi.useRealTimers(), subsequent tests would
run under fake timers and break. Wrap both fake timer tests in
try/finally to guarantee cleanup.
* fix: guard background revalidation against caching error responses
The stale-while-revalidate background refetch unconditionally cached
whatever response came back, including 500s. A transient server error
would permanently replace good cached data. Add a `response.ok` guard
matching the existing cache-miss path.
* fix: tighten cache guard to status === 200
Per review feedback, narrow the cache guard from `response.ok` (2xx)
to `response.status === 200` in both the background revalidation path
and the cache-miss path. Non-200 success codes (201, 204, 206) are
not meaningful to cache in the fetch cache context.
---------
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
* fix(fetch-cache): register stale-while-revalidate refetch with waitUntil()
The fetch cache's background refetch on stale entries was fire-and-forget.
On Cloudflare Workers, the isolate terminates after the response is sent,
killing in-flight refetches so stale entries never get refreshed.
Register the refetch promise with ExecutionContext.waitUntil() via the
existing getRequestExecutionContext() ALS accessor, matching the pattern
already used by ISR in isr-cache.ts.
* ci: re-trigger CI
* add oxfmt formatter: config, scripts, CI, editor setup, docs
* rebuild lockfile
* fix: add Format to required checks list, remove dead ignore pattern
* run fmt
* add format to agents.md again
* fix: bound fetch cache key body serialization
* Safeguard cache key serialization from large bodies
Bump cache key version to v2 and add safeguards to serializeBody to avoid OOM when processing large request bodies. Track cumulative raw byte size of streamed chunks and throw BodyTooLargeForCacheKeyError when exceeding the 1 MiB limit, call reader.cancel on errors, and add a cheap string-length pre-check. Adds tests covering oversized Uint8Array, string, multi-chunk ReadableStream, and FormData/File cases to verify such requests bypass the cache and still perform fetches.
---------
Co-authored-by: Sunil Pai <spai@cloudflare.com>
* fix: fetch cache key generation — handle all body types, include all headers minus blocklist
- Make buildFetchCacheKey async to support stream/blob body serialization
- Handle all BodyInit types: string, Uint8Array, ReadableStream, FormData, Blob, URLSearchParams
- Switch header strategy from allowlist (3 auth headers) to blocklist (traceparent/tracestate), matching Next.js
- Include RequestInit fields (mode, redirect, credentials, etc.) in cache key
- SHA-256 hash the cache key for compact, deterministic storage
- Fix spent stream bug: restore _ogBody in stripNextFromInit so originalFetch gets usable body
- Add 19 new tests covering body types, header inclusion, body restoration, and URLSearchParams
* Improve fetch cache key generation
Replace simple auth-header extraction with a unified header collector, add a header blocklist and cache version prefix, and introduce async cache key building. serializeBody now handles various body types (Uint8Array, streams, URLSearchParams, FormData, Blob, string), preserves the original body on init._ogBody, and body content is included in the cache key. The cache key is generated by normalizing URL, method, headers and init options and hashing them with SHA-256. buildFetchCacheKey is now async and awaited where used, and stripNextFromInit restores the preserved original body. Also add hasAuthHeaders and move auth header list into a constant.
* PR #111 approved. 52 tests pass, 3 fixes correct.
Co-authored-by: threepointone <threepointone@users.noreply.github.com>
---------
Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: threepointone <threepointone@users.noreply.github.com>