Skip to content

fix: rebuild reqCtx for rewrites after middleware#303

Merged
james-elicx merged 6 commits intomainfrom
james/fix-rewrite-req-ctx
Mar 6, 2026
Merged

fix: rebuild reqCtx for rewrites after middleware#303
james-elicx merged 6 commits intomainfrom
james/fix-rewrite-req-ctx

Conversation

@james-elicx
Copy link
Collaborator

@james-elicx james-elicx commented Mar 6, 2026

All three rewrite types — beforeFiles, afterFiles, and fallback — run after middleware per the Next.js execution order:

headers → redirects → Middleware → beforeFiles → filesystem → afterFiles → fallback

This applies to both App Router and Pages Router (confirmed in the Next.js 14 docs).

What was wrong

has/missing conditions on rewrite rules were evaluated against the pre-middleware request context, so middleware-injected cookies/headers were invisible to the matcher.

Additionally, beforeFiles rewrites were running before middleware in app-dev-server.ts — a pre-existing ordering bug, now fixed.

What changed

  • app-dev-server.ts: Moved beforeFiles block to after middleware execution. Added __buildPostMwRequestContext() helper that reads the ALS-stored headers context (post-middleware) and converts Map<string,string>Record<string,string> for cookie lookups. All three rewrite types now use this post-middleware context.
  • prod-server.ts: beforeFiles now uses postMwReqCtx (was incorrectly using the pre-middleware snapshot). headers and redirects correctly retain the pre-middleware context.
  • deploy.ts: Already correct for afterFiles/fallback; beforeFiles also switched to postMwReqCtx.
  • shims/headers.ts: Exported getHeadersContext() for use in the post-middleware context helper.

Tests added

Regression tests covering all three rewrite types across both routers:

Rewrite type Router Server under test
afterFiles Pages Router dev + prod (prod-server.ts)
beforeFiles App Router dev (app-dev-server.ts)
beforeFiles Pages Router prod (prod-server.ts)
fallback App Router dev (app-dev-server.ts)

Each test verifies: without ?mw-auth the rule does not match (404); with ?mw-auth middleware injects the cookie and the rule matches (200, rewrites to /about).

afterFiles and fallback rewrites run after middleware in the App Router
execution order. Their has/missing conditions should evaluate against
middleware-modified headers, not the original pre-middleware request.

Introduces postMwReqCtx (prod-server, deploy.ts) and __postMwReqCtx
(app-dev-server) built after x-middleware-request-* headers are unpacked.
headers, redirects, and beforeFiles rewrites keep using the original
pre-middleware reqCtx, matching Next.js execution order.

Adds getHeadersContext() to the headers shim so app-dev-server can read
the ALS-mutated headers context for the post-middleware snapshot.

Test: afterFiles rewrite with has:[cookie:mw-user] is skipped without
middleware injection and matches after middleware injects the cookie.
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 6, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@303

commit: 34e367d

@james-elicx james-elicx marked this pull request as draft March 6, 2026 21:15
@github-actions
Copy link

github-actions bot commented Mar 6, 2026

Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original

In App Router execution order, beforeFiles rewrites run after middleware
(step 4 vs step 3). deploy.ts processes them after middleware runs, so
they should use postMwReqCtx like afterFiles and fallback.

prod-server.ts (Pages Router) correctly keeps reqCtx for beforeFiles —
the Pages Router order has no middleware step before beforeFiles.
…d has/missing regression tests

- Move beforeFiles rewrite block after middleware execution so it sees
  middleware-injected cookies/headers (matches Next.js App Router order:
  middleware → beforeFiles → filesystem → afterFiles → fallback)
- Fix Map→Record bug in __buildPostMwRequestContext: ctx.cookies is a
  Map<string,string> but RequestContext.cookies must be Record<string,string>;
  convert with Object.fromEntries() so cookie lookups work correctly
- Update .rsc-suffix stripping test to reflect new code path (cleanPathname
  is now used directly by beforeFiles rather than a separate __rewritePathname)
- Add regression tests: beforeFiles and fallback rewrite has/missing conditions
  see middleware-injected cookies (app-router); fixtures wire up mw-gated-before
  and mw-gated-fallback routes + middleware cookie injection
Per Next.js docs, the execution order is: headers → redirects → Middleware
→ beforeFiles → filesystem → afterFiles → fallback. This applies to both
App Router and Pages Router. prod-server.ts was incorrectly using the
pre-middleware reqCtx for beforeFiles matching; switch to postMwReqCtx.

Add regression test: beforeFiles has/missing conditions on the production
Pages Router server now correctly see middleware-injected cookies.
@james-elicx
Copy link
Collaborator Author

/bigbonk review

Copy link
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix: rebuild reqCtx for afterFiles/fallback rewrites after middleware

Solid fix. The core problem is well-understood: in Next.js execution order, afterFiles and fallback rewrites (and beforeFiles per the docs) run after middleware, so their has/missing conditions must evaluate against the middleware-modified request, not the original pre-middleware snapshot.

What the PR does correctly

Consistent three-server fix. The change is applied to all three request-handling codepaths that need it:

  • app-dev-server.ts — introduces __buildPostMwRequestContext() reading from the live ALS HeadersContext after applyMiddlewareRequestHeaders() runs
  • prod-server.ts — rebuilds postMwReqCtx via requestContextFromRequest(webRequest) after unpacking x-middleware-request-* headers into the mutable webRequest
  • deploy.ts (Workers entry) — clones the immutable Workers Request with new headers, then rebuilds postMwReqCtx from the clone

All three pass postMwReqCtx to afterFiles, fallback, and beforeFiles rewrites while keeping the original reqCtx for headers and redirects — which matches Next.js semantics.

getHeadersContext() export. The new export in headers.ts is minimal and clean — it reads from the existing ALS state and returns the current HeadersContext (or null). No new state introduced.

__buildPostMwRequestContext() fallback. When getHeadersContext() returns null (no middleware, or middleware didn't modify anything), it falls back to __buildRequestContext(request), which is the right behavior — no middleware means pre/post are identical.

Test coverage. Both App Router and Pages Router get tests that verify:

  1. Without ?mw-auth → middleware doesn't inject cookie → has:[cookie:...] condition fails → no rewrite → 404
  2. With ?mw-auth → middleware injects cookie → condition matches → rewrite to /about → 200

The tests cover beforeFiles (App Router), afterFiles (Pages Router), and fallback (App Router), which is thorough.

Minor observations

Self-review comment addressed? james-elicx left a self-review suggesting let cleanPathname = pathname.replace(/\.rsc$/, ""); — looking at the generated code at line 1387, it currently uses let cleanPathname = pathname.replace(/\\.rsc$/, ""); with the double-backslash, which is correct since it's inside a template literal string that gets emitted as JavaScript (the \\ becomes \ in the output). The self-review suggestion to use a single \ would produce a regex that matches literal .rsc in the emitted code, which is what we want. Both are functionally equivalent since the generated code goes through string interpolation, but the single backslash is cleaner for readability. Either way, not a bug.

cookiesRecord conversion in __buildPostMwRequestContext(). The Object.fromEntries(ctx.cookies) conversion is necessary because the generated __checkSingleCondition function accesses cookies via ctx.cookies[condition.key] (bracket notation on a plain object), not Map.get(). Good that this is documented in the comment.

beforeFiles uses postMwReqCtx in all three servers. This is consistent with Next.js docs which state the execution order as: headers → redirects → Middleware → beforeFiles → filesystem → afterFiles → fallback. However, it's worth noting that the PR description says "headers, redirects, and beforeFiles rewrites keep using the original pre-middleware context" but the actual implementation passes postMwReqCtx to beforeFiles as well, which is correct per the Next.js docs. The PR description may need a minor correction on this point, but the code is right.

All CI checks passing (E2E, deploys, smoke tests). LGTM.

@ask-bonk
Copy link
Contributor

ask-bonk bot commented Mar 6, 2026

Review posted. Here's a summary:

Approved PR #303 — The fix correctly rebuilds the request context after middleware runs so that afterFiles, fallback, and beforeFiles rewrite has/missing conditions evaluate against middleware-modified headers/cookies.

Key findings:

  • Three-server consistency: The fix is properly applied to app-dev-server.ts, prod-server.ts, and deploy.ts (Workers entry), maintaining dev/prod parity
  • Clean API addition: getHeadersContext() in headers.ts is minimal — reads from existing ALS state with no new state introduced
  • Correct fallback: When no middleware is present, __buildPostMwRequestContext() falls back to the original request context
  • Good test coverage: Tests verify both "middleware doesn't inject cookie → 404" and "middleware injects cookie → rewrite matches → 200" for beforeFiles, afterFiles, and fallback rewrites across both App Router and Pages Router
  • Minor note: PR description says beforeFiles uses pre-middleware context, but the implementation correctly uses postMwReqCtx for beforeFiles too (matching Next.js docs execution order). Code is right; description could use a tweak.

github run

@james-elicx james-elicx changed the title fix: rebuild reqCtx for afterFiles/fallback rewrites after middleware fix: rebuild reqCtx for rewrites after middleware Mar 6, 2026
@james-elicx james-elicx marked this pull request as ready for review March 6, 2026 21:58
@james-elicx james-elicx merged commit 619e5c0 into main Mar 6, 2026
23 checks passed
@james-elicx james-elicx deleted the james/fix-rewrite-req-ctx branch March 6, 2026 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant