[PR #541] [MERGED] fix: eliminate double middleware execution in hybrid app+pages dev mode #656

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

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/541
Author: @NathanDrake2406
Created: 3/15/2026
Status: Merged
Merged: 3/16/2026
Merged by: @james-elicx

Base: mainHead: fix/double-middleware-execution


📝 Commits (5)

  • 77d6620 fix: eliminate double middleware execution in hybrid app+pages dev mode
  • 51c8fbe test: update entry template snapshot for middleware forwarding
  • adb293b fix: address code review findings for middleware forwarding
  • 7662eb9 test: update entry template snapshot for review fixes
  • 7c18ace fix: extract deferred headers helper, clarify flag placement per review

📊 Changes

5 files changed (+152 additions, -32 deletions)

View changed files

📝 packages/vinext/src/entries/app-rsc-entry.ts (+42 -0)
📝 packages/vinext/src/index.ts (+55 -5)
📝 tests/__snapshots__/entry-templates.test.ts.snap (+42 -0)
📝 tests/e2e/app-router/middleware.spec.ts (+5 -17)
📝 tests/fixtures/app-basic/instrumentation-state.ts (+8 -10)

📄 Description

Summary

  • In hybrid app+pages dev setups, middleware ran twice per App Router request — once in the connect handler (SSR env) and again in the RSC entry (RSC env). This caused double side effects for auth checks, logging, analytics, and any middleware state mutations.
  • The connect handler now forwards middleware results to the RSC entry via an x-vinext-mw-ctx request header. The RSC entry reconstructs _mwCtx from the forwarded data instead of re-executing the middleware function.
  • Pure App Router (no pages/) and pure Pages Router (no app/) setups are unaffected — they already use a single middleware execution path.

Key design decisions

  • Forwarding over skipping: The RSC entry needs _mwCtx.headers for applyMiddlewareRequestHeaders() (so headers() in Server Components sees middleware-modified headers). Simply skipping middleware would break this.
  • Explicit rewrite forwarding: The RSC plugin constructs its Request from the original HTTP request, not from req.url, so middleware rewrites must be forwarded explicitly in the header payload.
  • Response header deferral: When hasAppDir is true, middleware response headers are deferred (not applied to res) to avoid duplicates. They're applied only for Pages routes handled directly by the connect handler.
  • Production guard: The x-vinext-mw-ctx check is guarded by NODE_ENV !== "production" since the forwarding mechanism is dev-only. In production there is no connect handler, so an attacker-supplied header must not be trusted.

Test plan

  • Converted existing test.fixme to live regression test — verifies middlewareInvocationCount === 1 for App Router requests in hybrid fixture
  • All 7 middleware E2E tests pass (redirect, rewrite, rewrite+status, block, execution count)
  • Full app-router E2E suite: 283 passed, 0 failed (baseline: 282 passed + 1 fixme skip)
  • Build, typecheck, lint, format all clean
  • CI: full Vitest suite + all Playwright projects

🔄 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/541 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 3/15/2026 **Status:** ✅ Merged **Merged:** 3/16/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `fix/double-middleware-execution` --- ### 📝 Commits (5) - [`77d6620`](https://github.com/cloudflare/vinext/commit/77d6620b3fbfc44493d858ee8dca8fdeb276cba5) fix: eliminate double middleware execution in hybrid app+pages dev mode - [`51c8fbe`](https://github.com/cloudflare/vinext/commit/51c8fbe57bed938310ed011cb88c1e90ea950db7) test: update entry template snapshot for middleware forwarding - [`adb293b`](https://github.com/cloudflare/vinext/commit/adb293b095dbd83cc85b483ca7d95ba20268ca0e) fix: address code review findings for middleware forwarding - [`7662eb9`](https://github.com/cloudflare/vinext/commit/7662eb9df14303f16272dc6cb94c964b25a3f428) test: update entry template snapshot for review fixes - [`7c18ace`](https://github.com/cloudflare/vinext/commit/7c18ace3c1ac562ed858bbd963395c83b0ded5b4) fix: extract deferred headers helper, clarify flag placement per review ### 📊 Changes **5 files changed** (+152 additions, -32 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/entries/app-rsc-entry.ts` (+42 -0) 📝 `packages/vinext/src/index.ts` (+55 -5) 📝 `tests/__snapshots__/entry-templates.test.ts.snap` (+42 -0) 📝 `tests/e2e/app-router/middleware.spec.ts` (+5 -17) 📝 `tests/fixtures/app-basic/instrumentation-state.ts` (+8 -10) </details> ### 📄 Description ## Summary - In hybrid app+pages dev setups, middleware ran twice per App Router request — once in the connect handler (SSR env) and again in the RSC entry (RSC env). This caused double side effects for auth checks, logging, analytics, and any middleware state mutations. - The connect handler now forwards middleware results to the RSC entry via an `x-vinext-mw-ctx` request header. The RSC entry reconstructs `_mwCtx` from the forwarded data instead of re-executing the middleware function. - Pure App Router (no `pages/`) and pure Pages Router (no `app/`) setups are unaffected — they already use a single middleware execution path. ### Key design decisions - **Forwarding over skipping:** The RSC entry needs `_mwCtx.headers` for `applyMiddlewareRequestHeaders()` (so `headers()` in Server Components sees middleware-modified headers). Simply skipping middleware would break this. - **Explicit rewrite forwarding:** The RSC plugin constructs its Request from the original HTTP request, not from `req.url`, so middleware rewrites must be forwarded explicitly in the header payload. - **Response header deferral:** When `hasAppDir` is true, middleware response headers are deferred (not applied to `res`) to avoid duplicates. They're applied only for Pages routes handled directly by the connect handler. - **Production guard:** The `x-vinext-mw-ctx` check is guarded by `NODE_ENV !== "production"` since the forwarding mechanism is dev-only. In production there is no connect handler, so an attacker-supplied header must not be trusted. ## Test plan - [x] Converted existing `test.fixme` to live regression test — verifies `middlewareInvocationCount === 1` for App Router requests in hybrid fixture - [x] All 7 middleware E2E tests pass (redirect, rewrite, rewrite+status, block, execution count) - [x] Full app-router E2E suite: 283 passed, 0 failed (baseline: 282 passed + 1 fixme skip) - [x] Build, typecheck, lint, format all clean - [x] CI: full Vitest suite + all Playwright projects --- <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:22 +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#656
No description provided.