[PR #523] [MERGED] feat: add ISR caching for App Router route handlers #643

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

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/523
Author: @NathanDrake2406
Created: 3/13/2026
Status: Merged
Merged: 3/14/2026
Merged by: @james-elicx

Base: mainHead: feat/route-handler-isr-caching


📝 Commits (7)

  • 79e2606 feat: add ISR caching for App Router route handlers
  • e36a6b5 chore: update entry-templates snapshots for route handler ISR
  • cd83511 test: add auto-HEAD ISR cache test for route handlers
  • d4c52b5 fix: review findings — force-dynamic guard, consistent header filtering in cache writes
  • 5015641 test: poll for ISR background regen instead of fixed sleep
  • 7773123 fix: capture request.url before regen closure to avoid pinning Request
  • 2b391b5 fix: address remaining review findings

📊 Changes

5 files changed (+953 additions, -14 deletions)

View changed files

📝 packages/vinext/src/entries/app-rsc-entry.ts (+115 -2)
📝 tests/__snapshots__/entry-templates.test.ts.snap (+690 -12)
📝 tests/app-router.test.ts (+130 -0)
tests/fixtures/app-basic/app/api/force-dynamic-revalidate/route.ts (+6 -0)
📝 tests/nextjs-compat/app-routes.test.ts (+12 -0)

📄 Description

Summary

  • Route handlers with export const revalidate = N now get server-side ISR caching (MISS → HIT → STALE with background regeneration), matching the existing page ISR behavior
  • Previously only Cache-Control headers were emitted — the handler re-executed on every request with no server-side cache lookup or store
  • Reuses existing CachedRouteValue type (kind: "APP_ROUTE") and KV serialization that were already defined but unused

Implementation

  • __isrRouteKey helper with "route" suffix for cache key construction (no HTML/RSC split needed)
  • Cache READ before handler execution — HIT returns cached response immediately (skips handler), STALE serves stale data and triggers background regeneration
  • Cache WRITE on MISS via response.clone() + waitUntil — stores raw handler response before cookie/middleware transforms
  • Background regen uses synthetic request + clean ALS context (empty headers/cookies) to prevent user-specific data from leaking into cached responses
  • Guards: production-only, GET/HEAD only, skips dynamic handlers (headers()/cookies()), skips handlers that set own Cache-Control, filters Infinity from revalidateSeconds

Test Plan

  • 3 code generation tests (__isrRouteKey, APP_ROUTE cache read/write in generated code)
  • 6 production behavioral tests (MISS, HIT, POST bypass, dynamic bypass, handler Cache-Control bypass, STALE lifecycle)
  • 1 dev-mode guard test (no X-Vinext-Cache header in dev)
  • 262/262 app-router.test.ts pass (no regressions to page ISR)
  • 28/28 app-routes.test.ts pass
  • 78/78 isr-cache.test.ts + kv-cache-handler.test.ts pass
  • Typecheck clean, lint clean, format clean

🔄 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/523 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 3/13/2026 **Status:** ✅ Merged **Merged:** 3/14/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `feat/route-handler-isr-caching` --- ### 📝 Commits (7) - [`79e2606`](https://github.com/cloudflare/vinext/commit/79e2606a5d3a6b285a93e25acbeccf52e9ddb80f) feat: add ISR caching for App Router route handlers - [`e36a6b5`](https://github.com/cloudflare/vinext/commit/e36a6b5f8dc1c38db4d0ab8781659840262df896) chore: update entry-templates snapshots for route handler ISR - [`cd83511`](https://github.com/cloudflare/vinext/commit/cd8351148feeab52b6d0c4fb5ef44634d3e4e1f9) test: add auto-HEAD ISR cache test for route handlers - [`d4c52b5`](https://github.com/cloudflare/vinext/commit/d4c52b5b4b5c59ed868ccce867e381650728a518) fix: review findings — force-dynamic guard, consistent header filtering in cache writes - [`5015641`](https://github.com/cloudflare/vinext/commit/50156418c3e4fc8f998bca0973055f0b2ce90347) test: poll for ISR background regen instead of fixed sleep - [`7773123`](https://github.com/cloudflare/vinext/commit/77731230b6047b15371c307d5b3d0dc2e2611eb6) fix: capture request.url before regen closure to avoid pinning Request - [`2b391b5`](https://github.com/cloudflare/vinext/commit/2b391b5f7c501caf65ad66a6ecb108c8c77a769d) fix: address remaining review findings ### 📊 Changes **5 files changed** (+953 additions, -14 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/entries/app-rsc-entry.ts` (+115 -2) 📝 `tests/__snapshots__/entry-templates.test.ts.snap` (+690 -12) 📝 `tests/app-router.test.ts` (+130 -0) ➕ `tests/fixtures/app-basic/app/api/force-dynamic-revalidate/route.ts` (+6 -0) 📝 `tests/nextjs-compat/app-routes.test.ts` (+12 -0) </details> ### 📄 Description ## Summary - Route handlers with `export const revalidate = N` now get server-side ISR caching (MISS → HIT → STALE with background regeneration), matching the existing page ISR behavior - Previously only `Cache-Control` headers were emitted — the handler re-executed on every request with no server-side cache lookup or store - Reuses existing `CachedRouteValue` type (`kind: "APP_ROUTE"`) and KV serialization that were already defined but unused ### Implementation - **`__isrRouteKey`** helper with `"route"` suffix for cache key construction (no HTML/RSC split needed) - **Cache READ** before handler execution — HIT returns cached response immediately (skips handler), STALE serves stale data and triggers background regeneration - **Cache WRITE** on MISS via `response.clone()` + `waitUntil` — stores raw handler response before cookie/middleware transforms - **Background regen** uses synthetic request + clean ALS context (empty headers/cookies) to prevent user-specific data from leaking into cached responses - **Guards**: production-only, GET/HEAD only, skips dynamic handlers (`headers()`/`cookies()`), skips handlers that set own `Cache-Control`, filters `Infinity` from `revalidateSeconds` ## Test Plan - [x] 3 code generation tests (`__isrRouteKey`, `APP_ROUTE` cache read/write in generated code) - [x] 6 production behavioral tests (MISS, HIT, POST bypass, dynamic bypass, handler Cache-Control bypass, STALE lifecycle) - [x] 1 dev-mode guard test (no `X-Vinext-Cache` header in dev) - [x] 262/262 `app-router.test.ts` pass (no regressions to page ISR) - [x] 28/28 `app-routes.test.ts` pass - [x] 78/78 `isr-cache.test.ts` + `kv-cache-handler.test.ts` pass - [x] Typecheck clean, lint clean, format clean --- <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:16 +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#643
No description provided.