[PR #613] [CLOSED] fix: detect dynamic API usage in route handlers to prevent stale ISR cache hits #713

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

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/613
Author: @southpolesteve
Created: 3/20/2026
Status: Closed

Base: mainHead: fix/route-handler-dynamic-detection-v2


📝 Commits (2)

  • 72e3078 fix: detect dynamic API usage in route handlers to prevent stale ISR cache hits
  • e0b9bee debug: pass raw request to handler (test if proxy is the cause)

📊 Changes

6 files changed (+417 additions, -42 deletions)

View changed files

📝 packages/vinext/src/build/prerender.ts (+1 -1)
📝 packages/vinext/src/entries/app-rsc-entry.ts (+55 -2)
📝 packages/vinext/src/shims/headers.ts (+12 -8)
📝 packages/vinext/src/shims/unified-request-context.ts (+1 -1)
📝 tests/__snapshots__/entry-templates.test.ts.snap (+330 -12)
📝 tests/unified-request-context.test.ts (+18 -18)

📄 Description

Summary

Route handlers that access request.headers or request.nextUrl.searchParams are dynamic and should not be served from the ISR cache. This PR adds runtime dynamic detection matching Next.js behavior.

Changes

  1. __proxyRouteRequest: Wraps the Request passed to route handlers in a Proxy. Accessing .headers or .nextUrl.searchParams calls markDynamicUsage(). Methods are bound to the original target to preserve Web API internal slots (.json(), .text(), etc.).

  2. __dynamicRouteHandlers: A module-level Set that remembers route patterns whose handlers used dynamic APIs. The ISR cache read condition checks this set and skips the cache for known-dynamic handlers.

  3. dynamicUsageCount: Replaces the boolean dynamicUsageDetected flag with a counter, enabling future counter-based bracketing of handler execution.

Why not query-string-in-cache-key?

James pointed out that including query strings in the cache key diverges from Next.js, which uses pathname-only keys (source). In Next.js, handlers that read searchParams are detected as dynamic and never cached at all. This PR matches that behavior.

Proxy granularity

Access Dynamic? Reason
request.method No Same for all ISR requests
request.url (string) No Can't track downstream usage
request.nextUrl.pathname No Pathname is the route
request.nextUrl.searchParams Yes Varies per request
request.headers Yes Varies per request

The one gap (same as Next.js): new URL(request.url).searchParams bypasses the Proxy.


🔄 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/613 **Author:** [@southpolesteve](https://github.com/southpolesteve) **Created:** 3/20/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/route-handler-dynamic-detection-v2` --- ### 📝 Commits (2) - [`72e3078`](https://github.com/cloudflare/vinext/commit/72e3078f0abfe547a30cfad50dee0177038b534a) fix: detect dynamic API usage in route handlers to prevent stale ISR cache hits - [`e0b9bee`](https://github.com/cloudflare/vinext/commit/e0b9bee4efc2bc23b16c5cef7327b04d8896fa43) debug: pass raw request to handler (test if proxy is the cause) ### 📊 Changes **6 files changed** (+417 additions, -42 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/build/prerender.ts` (+1 -1) 📝 `packages/vinext/src/entries/app-rsc-entry.ts` (+55 -2) 📝 `packages/vinext/src/shims/headers.ts` (+12 -8) 📝 `packages/vinext/src/shims/unified-request-context.ts` (+1 -1) 📝 `tests/__snapshots__/entry-templates.test.ts.snap` (+330 -12) 📝 `tests/unified-request-context.test.ts` (+18 -18) </details> ### 📄 Description ## Summary Route handlers that access `request.headers` or `request.nextUrl.searchParams` are dynamic and should not be served from the ISR cache. This PR adds runtime dynamic detection matching Next.js behavior. ## Changes 1. **`__proxyRouteRequest`**: Wraps the `Request` passed to route handlers in a Proxy. Accessing `.headers` or `.nextUrl.searchParams` calls `markDynamicUsage()`. Methods are bound to the original target to preserve Web API internal slots (`.json()`, `.text()`, etc.). 2. **`__dynamicRouteHandlers`**: A module-level `Set` that remembers route patterns whose handlers used dynamic APIs. The ISR cache read condition checks this set and skips the cache for known-dynamic handlers. 3. **`dynamicUsageCount`**: Replaces the boolean `dynamicUsageDetected` flag with a counter, enabling future counter-based bracketing of handler execution. ## Why not query-string-in-cache-key? [James pointed out](https://github.com/cloudflare/vinext/pull/603#pullrequestreview-2903462834) that including query strings in the cache key diverges from Next.js, which uses pathname-only keys ([source](https://github.com/vercel/next.js/blob/9f181bd11af5f532c529a6f168fe40505f0a4d75/packages/next/src/build/templates/app-route.ts#L181-L187)). In Next.js, handlers that read searchParams are detected as dynamic and never cached at all. This PR matches that behavior. ## Proxy granularity | Access | Dynamic? | Reason | |---|---|---| | `request.method` | No | Same for all ISR requests | | `request.url` (string) | No | Can't track downstream usage | | `request.nextUrl.pathname` | No | Pathname is the route | | `request.nextUrl.searchParams` | **Yes** | Varies per request | | `request.headers` | **Yes** | Varies per request | The one gap (same as Next.js): `new URL(request.url).searchParams` bypasses the Proxy. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:09:44 +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#713
No description provided.