Skip to content

fix(middleware): match Pages data request metadata#2239

Open
james-elicx wants to merge 7 commits into
mainfrom
codex/fix-middleware-general-data-parity-27858251903
Open

fix(middleware): match Pages data request metadata#2239
james-elicx wants to merge 7 commits into
mainfrom
codex/fix-middleware-general-data-parity-27858251903

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • expose trusted _next/data classification to middleware without trusting spoofable headers
  • emit Next.js-compatible matched-path metadata for dynamic, i18n, redirect, and miss flows
  • use the Pages data soft-redirect protocol and preserve App-to-Pages fallback parity

Validation

  • 165 focused middleware/pipeline/App tests
  • 23 focused Pages i18n tests
  • targeted dev and Playwright fixture probes
  • scoped vp check and final independent review: clean

Next.js parity

Targets middleware-general/test/node-runtime.test.ts and related middleware-general assertions from deploy-suite run 27858251903. Investigation used vinext fixtures; no raw Next.js E2E was run locally.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jun 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2239
npm i https://pkg.pr.new/vinext@2239

commit: 0ae6371

@github-actions

Copy link
Copy Markdown
Contributor
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
workers-cache preview production
web preview production

@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 0ae6371 against base cf9d601 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 132.8 KB 132.8 KB ⚫ +0.0%
Client entry size (gzip) vinext 118.7 KB 118.7 KB ⚫ +0.0%
Dev server cold start vinext 2.39 s 2.42 s ⚫ +0.9%
Production build time vinext 2.99 s 2.99 s ⚫ +0.2%
RSC entry closure size (gzip) vinext 92.2 KB 92.2 KB ⚫ +0.0%
Server bundle size (gzip) vinext 166.9 KB 166.9 KB ⚫ +0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: matched-path data-request metadata parity

I reviewed the diff against Next.js behavior and ran the focused suites (pages-request-pipeline, app-rsc-handler, middleware-runtime — 165 passing; the dev i18n features test passing). The core of this PR is solid and the security framing is correct.

What's good

  • Dropping the spoofable header is the right call and matches Next.js. Removing hasDataRequestHeader = rawRequest.headers.get("x-nextjs-data") === "1" and relying only on the trusted pagesDataNormalization?.isDataReq classification mirrors Next.js, which derives isNextDataRequest from URL normalization, not headers. The two new App Router tests (forged-header config redirect, forged-header middleware) lock this in well.
  • __isData exactly matches Next.js. The Object.defineProperty(nextRequest, "__isData", { enumerable: false, value: true }) in middleware-runtime.ts:321 is byte-for-byte the Next.js descriptor in packages/next/src/server/web/adapter.ts, and it's gated on the trusted isDataRequest, which corresponds to Next's isNextDataRequest. Good parity.
  • isDataRequest vs isMiddlewareDataRequest split in app-rsc-handler is principled — the soft-redirect/__isData protocol only fires on the trusted classification, and the new test asserts forged headers no longer leak into middleware.
  • dev/prod parity for the success path is handled correctly. Because dev omits renderPage from pipelineDeps (pipeline emits intents), the pipeline's matched-path logic can't run in dev, so the header is set directly in createSSRHandler (dev-server.ts:1445). The prod matchPageRoute already returns the full route object including pattern, so matchedPathnameForRoute works in prod too.

Issues

1. (non-blocking) request.__isData is exposed at runtime but not in the NextRequest type. The fixture tests/fixtures/pages-basic/middleware.ts:40 reads request.__isData, and the shim class in packages/vinext/src/shims/next-shims.d.ts:531 has no such member and no index signature. Typechecking the fixture's own tsconfig reports:

tests/fixtures/pages-basic/middleware.ts(40,69): error TS2339: Property '__isData' does not exist on type 'NextRequest'.

This does not fail CI (vp check doesn't typecheck fixtures), but any end user writing the same Next.js-style middleware under strict TS would hit this error. Next.js exposes __isData and users rely on it for testing. Suggest adding readonly __isData?: boolean; to the NextRequest shim class so the runtime feature is reflected in the type.

2. (worth confirming) dev data-miss does not get the middleware soft-200 + matched-path treatment. The new behavior — when middleware ran and status is null/200/404, a _next/data miss returns 200 {} with x-nextjs-matched-path — is implemented in the Pages pipeline (pages-request-pipeline.ts:698) and App Router (app-rsc-handler.ts:889), both gated on deps.renderPage/runMiddleware. In dev, renderPage is omitted so the pipeline branch can't run, and the dev-server's own miss handler (dev-server.ts:545) still returns a plain 404 {} with no x-nextjs-matched-path, even when middleware is present. The added e2e/unit tests cover the rewrite-success and prod miss cases but not a dev data-miss-with-middleware. If Next.js returns the soft-200 here, this is a dev/prod parity gap; please confirm against the middleware-general suite and either replicate it in dev-server.ts or document why dev differs.

Verified, no action needed

  • matchedPathnameForRoute i18n branches are correct: explicit-locale path → /<locale><pattern>; default-locale path → normalizeDefaultLocalePathname prepends the default locale, matching Next's always-localized x-nextjs-matched-path.
  • Pages miss uses original pathname and App Router uses canonicalPathname — both intentionally the original request path, consistent with each other.
  • buildNextDataNotFoundResponse() header copy in app-rsc-handler.ts:895 correctly preserves the deployment-id header before overriding status/body.
  • The dev matchPageRoute i18n normalization (index.ts:4091) now matches the prod matchPageRoute (pages-server-entry.ts:315) — this actually closes a prior dev i18n matching gap rather than introducing drift.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2239. Summary of my findings:

Review summary

The PR is well-constructed and the security framing is correct. I ran the focused suites (165 tests passing) and verified the implementation against Next.js source.

Strong points:

  • Dropping the spoofable x-nextjs-data header in favor of trusted URL normalization matches Next.js exactly (isNextDataRequest is URL-derived, not header-derived).
  • The __isData defineProperty is byte-for-byte identical to Next.js's adapter.ts, gated on the trusted classification.
  • dev/prod parity for the success path is handled correctly (dev sets the header directly in createSSRHandler since the pipeline can't run without renderPage).

Two issues flagged:

  1. (non-blocking) __isData type gap — the runtime exposes request.__isData but the NextRequest shim type doesn't declare it. Confirmed via tsc on the fixture tsconfig: TS2339: Property '__isData' does not exist on type 'NextRequest'. Doesn't fail CI (fixtures aren't typechecked by vp check), but real users with strict TS would hit it. Suggested adding readonly __isData?: boolean; to the shim class.

  2. (confirm) dev data-miss parity — the new middleware soft-200 + x-nextjs-matched-path behavior for _next/data misses runs in the Pages pipeline and App Router, but in dev the miss handler (dev-server.ts:545) still returns a plain 404 with no matched-path. Asked the author to confirm against the middleware-general suite whether dev should replicate it.

I also explicitly verified the i18n locale-prefixing logic, the original-vs-resolved pathname choices, the deployment-id header preservation, and that the dev matchPageRoute i18n change closes a prior gap rather than introducing drift — all correct, no action needed.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review: Pages data-request metadata parity (follow-up on the prior ask-bonk review)

I re-reviewed the diff against the Next.js source (canary) and the middleware-general suite (the suite this PR targets). The prior review covered the security framing well; this pass focuses on behavioral parity for the new matched-path / soft-redirect logic, where I found three divergences from Next.js that the current tests lock in as "correct."

1. (parity) Dynamic data routes emit the route pattern, not the interpolated path

matchedPathnameForRoute() returns patternToNextFormat(route.pattern), so a dynamic data request resolves to /blog/[slug]. The new test at tests/pages-request-pipeline.test.ts:257 ("uses the matched route pattern for dynamic data responses") asserts exactly /blog/[slug], and dev does the same (dev-server.ts:1450).

Next.js sets x-nextjs-matched-path to the interpolated concrete path (/blog/123), not the bracket pattern. See base-server.ts:1386 (matchedPath = utils.interpolateDynamicPath(srcPathname, params)) feeding parsedUrl.pathname → the matched-path header at router-server.ts:322.

This matters functionally: the client router branches on isDynamicRoute(matched-path) (shared/lib/router/router.ts:220). /blog/[slug] is dynamic; /blog/123 is not, so the two take different resolution paths. The middleware-general suite only rewrites the static /ssr-page → /ssr-page-2 (app/middleware.js:246), so it won't catch this — but it's a real divergence baked into the new tests. Please confirm against a dynamic _next/data request what Next.js actually emits, and align the value (interpolated path) rather than the pattern.

2. (parity) Config redirects now emit a soft-200 x-nextjs-redirect; Next.js sends a real 307/308

The new branch at pages-request-pipeline.ts:298 converts next.config.js redirects into status: 200 + x-nextjs-redirect for trusted data requests, and tests/pages-request-pipeline.test.ts:96 locks this in ("uses the soft redirect protocol for trusted data requests" → status 200).

In Next.js, x-nextjs-redirect is emitted only by the middleware adapter (web/adapter.ts:494-499), and only there is Location swapped for the internal header. Config-file redirects go through resolve-routes.ts:800-826 and return a real redirect status (getRedirectStatus(route) → 307/308) with a normal Location, even for _next/data requests. The client handles those data redirects at the HTTP-status level when middleware is present (shared/lib/router/router.ts:534-538, [301,302,307,308]).

So emitting a soft-200 for config redirects changes observable behavior (status 307/308 → 200) versus Next.js. The header-based path still works on the client, but this is a deliberate divergence that isn't documented as one. Please confirm whether Next.js really soft-redirects config redirects on data requests (I don't believe it does) — if not, config redirects should keep emitting the real 307/308 and the soft-redirect protocol should stay scoped to middleware-issued redirects.

3. (dev/prod parity) i18n miss matched-path differs between dev and prod

For a data miss with middleware:

  • Prod (pages-request-pipeline.ts:707) sets matched-path = matchResolvedPathname(pathname), which preserves the locale prefix (e.g. /fr/missing stays /fr/missing, default locale gets normalized in).
  • Dev (dev-server.ts:555) sets matched-path = localeStrippedUrl, i.e. the locale-stripped path (/fr/missing/missing).

Next.js's miss branch uses `${locale ? '/'+locale : ''}${pathname}` (base-server.ts:2733-2736) — locale-prefixed. So dev drops the locale and disagrees with both prod and Next.js for non-default-locale misses. Either reuse the locale-prefixed value in dev or add a dev i18n miss test to pin the intended behavior.

Verified, no action needed

  • Dropping the spoofable x-nextjs-data header read and gating on the trusted pagesDataNormalization.isDataReq classification matches Next.js (isNextDataRequest is URL-derived). The forged-header tests are good.
  • __isData (middleware-runtime.ts:321) is byte-identical to Next.js's adapter.ts descriptor and gated on the trusted classification. The error-throw fixture mirrors middleware-general/app/middleware.js:251.
  • App Router data miss (app-rsc-handler.ts:887-900) uses the original canonicalPathname and preserves the deployment-id header — consistent with a true miss.
  • The miss-branch status gate (null | 200 | 404) matches Next.js base-server.ts:2726-2729.
  • The dev matchPageRoute i18n change (index.ts:4091) now mirrors prod's matchPageRoute, closing a prior gap.

Suggestions carried over from the prior review (still open)

  • request.__isData is exposed at runtime but missing from the NextRequest shim type — the fixture middleware.ts:40 reads it and strict-TS users would hit TS2339. Add readonly __isData?: boolean; to the shim.

Net: the security model is right and the success/miss plumbing is close, but items 1 and 2 encode behavior that differs from Next.js into new passing tests, which is the kind of silent divergence AGENTS.md warns about. Worth confirming each against a real Next.js _next/data response before merge.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2239. Summary of findings:

Review summary

I re-reviewed against Next.js canary source and the middleware-general suite this PR targets, focusing on the new matched-path / soft-redirect parity (the prior review covered the security framing). I found three divergences from Next.js that the new tests lock in as correct:

  1. (parity) Dynamic data routes emit the route pattern /blog/[slug], not the interpolated path /blog/123. matchedPathnameForRoute() uses patternToNextFormat(route.pattern); Next.js uses interpolateDynamicPath (base-server.ts:1386). This matters because the client router branches on isDynamicRoute(matched-path) (router.ts:220). The middleware-general fixture only rewrites a static route so it won't catch it, but the new test (pages-request-pipeline.test.ts:257) asserts the pattern.

  2. (parity) Config redirects now emit a soft-200 x-nextjs-redirect; Next.js sends a real 307/308. x-nextjs-redirect is exclusively a middleware-adapter feature in Next.js (web/adapter.ts:494). Config-file redirects return a real redirect status (resolve-routes.ts:825), and the client handles data redirects at the HTTP-status level (router.ts:534). The new branch (pages-request-pipeline.ts:298) changes observable status from 307/308 → 200 for config redirects.

  3. (dev/prod parity) i18n miss matched-path differs. Prod preserves the locale prefix (matchResolvedPathname(pathname)); dev uses the locale-stripped URL (dev-server.ts:555). Next.js is locale-prefixed (base-server.ts:2733).

I also confirmed the spoofable-header removal, __isData descriptor parity, App Router miss path, and the dev matchPageRoute i18n fix are all correct, and carried over the still-open __isData shim-type suggestion from the prior review.

The security model is right; items 1 and 2 are the kind of silent divergence (encoded into passing tests) that AGENTS.md warns about — worth confirming against a real Next.js _next/data response before merge.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: Pages data-request metadata parity — verified against Next.js canary source

I cloned vercel/next.js@canary and traced the actual emit paths for x-nextjs-matched-path, x-nextjs-redirect, and the data-miss soft-200 protocol, then re-ran the focused suites (165 passing). The security framing (dropping the spoofable x-nextjs-data header, gating on trusted isDataReq, byte-identical __isData descriptor) is correct and well covered.

Three of the prior ask-bonk findings deserve correction/confirmation. I disagree with item 1, confirm item 2, and confirm item 3.

1. (NOT a bug — prior review was wrong) Dynamic data routes correctly emit the route pattern /blog/[slug]

The prior review claimed Next.js emits the interpolated path (/blog/123) and pointed at interpolateDynamicPath (base-server.ts:1386). That is the wrong code path — it's the minimal-mode branch that consumes an upstream x-matched-path header to re-derive params, not the path that emits the x-nextjs-matched-path response header on a normal middleware data request.

The actual emit paths are base-server.ts:2205-2208 (success) and 2733-2736 (miss), both using the local pathname variable. That pathname is set from match.definition.pathname at the render dispatch (base-server.ts:2644-2647):

const result = await this.renderPageComponent(
  { ...ctx, pathname: match.definition.pathname, ... },

and RouteDefinition.pathname is documented as "The pathname (including dynamic placeholders)" (route-definitions/route-definition.ts:15-17) — i.e. the bracket pattern /blog/[slug].

So vinext's matchedPathnameForRoute()patternToNextFormat(route.pattern) and the test assertion /blog/[slug] (tests/pages-request-pipeline.test.ts:277) match Next.js exactly. No change needed. The dev success path (dev-server.ts:1450) is also correct.

2. (confirmed parity gap) Config redirects emit a soft-200; Next.js sends a real 307/308

pages-request-pipeline.ts:299-307 converts next.config.js redirects on trusted data requests into status: 200 + x-nextjs-redirect, locked in by tests/pages-request-pipeline.test.ts:96 (status 200).

In Next.js, config-file redirects resolve in resolve-routes.ts:800-826 and return a real redirect statusstatusCode: getRedirectStatus(route) (307/308) with a normal Location — with no _next/data special-casing. x-nextjs-redirect is emitted only by the middleware adapter (web/adapter.ts). The client handles middleware-present data redirects at the HTTP-status level for [301,302,307,308] (shared/lib/router/router.ts:534-538).

Both paths happen to work on the client (the soft-200 x-nextjs-redirect branch is also handled at router.ts:304-326), so this isn't a functional break — but it changes the observable HTTP status (307/308 → 200) for config redirects and bakes that into a passing test. This is exactly the "silent divergence encoded as correct" pattern AGENTS.md warns about. Recommend keeping the soft-redirect protocol scoped to middleware-issued redirects and letting config redirects keep their real 307/308, unless you can point to a Next.js test that soft-redirects config redirects on data requests.

3. (confirmed dev/prod parity gap) i18n data-miss matched-path drops the locale in dev

For a _next/data miss with middleware:

  • Prod (pages-request-pipeline.ts:708): matchResolvedPathname(pathname)normalizeDefaultLocalePathname, which preserves an explicit non-default locale (/fr/missing stays /fr/missing, normalizeDefaultLocalePathname line 92) and prefixes the default locale (/missing/en/missing).
  • Dev (dev-server.ts:555): uses localeStrippedUrl, so /fr/missing/missinglocale dropped.

Next.js's miss branch (base-server.ts:2733-2736) is `${locale ? '/'+locale : ''}${pathname}` — locale-prefixed, matching prod. Note dev's own success path (dev-server.ts:1450) already does `${locale ? '/'+locale : ''}${patternToNextFormat(route.pattern)}`, so the miss path is inconsistent with both prod and dev-success. Recommend making dev-server.ts:555 locale-prefixed (e.g. `${locale ? '/'+locale : ''}${localeStrippedUrl}`) and adding a dev i18n data-miss test — there's currently no coverage for it (the only dev i18n data test, features.test.ts:1412, is the success path).

Minor (non-blocking): request.__isData missing from the NextRequest shim type

Runtime exposes __isData (middleware-runtime.ts:325) and the fixture reads it (tests/fixtures/pages-basic/middleware.ts:40), but the NextRequest shim class (shims/next-shims.d.ts:531) doesn't declare it. CI doesn't typecheck fixtures so this is green, but strict-TS users writing the same middleware hit TS2339. Add readonly __isData?: boolean;.

Verified, no action needed

  • Spoofable x-nextjs-data removal + trusted isDataReq gating matches Next.js (isNextDataRequest is URL-derived).
  • __isData descriptor is byte-identical to web/adapter.ts.
  • App Router miss (app-rsc-handler.ts:887-900) uses original canonicalPathname, preserves the deployment-id header, and gates on real runMiddleware + null/200/404 — matches base-server.ts:2726-2729.
  • Dev matchPageRoute i18n normalization (index.ts) now mirrors prod, closing a prior gap.

Net: Item 1 is fine as-is (and the prior review was mistaken). Items 2 and 3 are real divergences worth resolving (or deliberately documenting) before merge; both are encoded into passing tests today.

@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2239. I verified all claims against a fresh clone of vercel/next.js@canary and re-ran the focused suites (165 passing).

Findings

The security framing is correct — dropping the spoofable x-nextjs-data header and gating on the trusted URL-derived isDataReq classification matches Next.js. The __isData descriptor is byte-identical to web/adapter.ts.

Three parity items, with a correction to the prior reviews:

  1. Dynamic data routes → I disagree with the prior review. It claimed Next.js emits the interpolated path (/blog/123) via interpolateDynamicPath. That's the wrong code path — that's the minimal-mode branch that consumes an upstream header, not the one that emits x-nextjs-matched-path. The actual emit paths (base-server.ts:2205, 2733) use match.definition.pathname, which is documented as "including dynamic placeholders" = the bracket pattern. So vinext's /blog/[slug] is correct as-is.

  2. Config redirect soft-200 → confirmed divergence. Next.js sends a real 307/308 for next.config.js redirects (resolve-routes.ts:825); vinext converts them to a soft-200 x-nextjs-redirect. Both work on the client, but it changes the observable status and bakes it into a passing test. The soft-redirect protocol should stay scoped to middleware-issued redirects.

  3. Dev i18n data-miss → confirmed dev/prod gap. Dev uses localeStrippedUrl (/fr/missing/missing), dropping the locale; prod and Next.js are locale-prefixed. Dev's own success path is already locale-prefixed, so the miss path is internally inconsistent and untested.

Plus one minor non-blocking note: request.__isData is missing from the NextRequest shim type (strict-TS users would hit TS2339).

Net: item 1 needs no change (prior review was mistaken); items 2 and 3 are real divergences worth resolving or deliberately documenting before merge.

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: Pages data-request metadata parity — re-review after ece4cdc

I re-reviewed the full diff against vercel/next.js@canary (freshly cloned) and re-ran the focused suites: 165 passing (pages-request-pipeline, app-rsc-handler, middleware-runtime), plus the Pages Router + features integration suites (654 passing) and vp check clean on all changed source files. The two open parity items from my prior review are now fixed, and the disputed item 1 is confirmed correct.

The three prior findings are resolved in ece4cdc

  1. Config redirect soft-200 → real 307/308 (fixed). The isDataReq || isDataRequest soft-200 branch was removed from both pages-request-pipeline.ts (config redirects now fall through to the real redirect.permanent ? 308 : 307) and app-rsc-handler.ts:556-571. The x-nextjs-redirect soft-redirect protocol now lives only in middleware-runtime.ts:184-191 (dataRedirectResponse), gated on the trusted options.isDataRequest at line 409 — which matches Next.js, where x-nextjs-redirect is emitted exclusively by the middleware adapter (web/adapter.ts) and config redirects keep their real status (resolve-routes.ts). Tests updated accordingly (pages-request-pipeline.test.ts:96, app-rsc-handler.test.ts:601 now assert 308 + real Location).

  2. Dev i18n data-miss now locale-prefixed (fixed). dev-server.ts:555 changed from localeStrippedUrl to `${locale ? `/${locale}` : ""}${localeStrippedUrl}`, matching Next.js base-server.ts:2734 (`${locale ? '/'+locale : ''}${pathname}`) and prod's matchResolvedPathname(pathname). Two new dev tests pin it: /fr/missing/fr/missing, /missing/en/missing (features.test.ts:1424, :1438), both passing.

  3. __isData shim type (fixed). readonly __isData?: boolean; added to the NextRequest class in next-shims.d.ts:531; the fixture middleware.ts:40 read no longer trips TS2339.

Item 1 (dynamic data route pattern) — confirmed correct as-is, no change needed

I traced the emit path in the freshly cloned canary. The success-path x-nextjs-matched-path is set at base-server.ts:2205-2207 from the local pathname, which is assigned match.definition.pathname at the render dispatch (base-server.ts:2647). RouteDefinition.pathname is documented as "The pathname (including dynamic placeholders)" (route-definitions/route-definition.ts:15-17) — i.e. the bracket pattern. So vinext's matchedPathnameForRoute()patternToNextFormat(route.pattern) and the /blog/[slug] assertion (pages-request-pipeline.test.ts) match Next.js. The interpolateDynamicPath path (base-server.ts:1386) is the minimal-mode branch that consumes an upstream header, not the emit path. No change.

Security model — verified solid

  • Spoofable x-nextjs-data header read is gone; entry-templates.test.ts:1024 asserts the generated entry no longer contains request.headers.get("x-nextjs-data"). Classification is URL-derived (isDataReq), matching Next.js isNextDataRequest.
  • __isData (middleware-runtime.ts:321-326) is byte-identical to Next.js's adapter.ts descriptor and gated on the trusted classification.
  • The new e2e (middleware.spec.ts) proves it end-to-end: a forged x-nextjs-data: 1 on a non-data path returns 200 (no throw), while a real _next/data URL returns 500 — request.__isData is trusted, not header-derived.
  • App Router miss (app-rsc-handler.ts:881-894) preserves the deployment-id header (buildNextDataNotFoundResponse sets it, copied via new Headers(response.headers)) and gates on real runMiddleware + null/200/404, matching base-server.ts:2726-2729.
  • hasMiddleware is now threaded consistently through all three adapters (dev index.ts:4088, prod prod-server.ts:1526/1793, worker deploy.ts:690) and gates the Pages soft-miss at pages-request-pipeline.ts:692.

One minor, non-blocking note (dev-only edge case)

The dev miss matched-path can include a query string where prod and Next.js do not. dev-server.ts:555-556 builds the header from localeStrippedUrl, and for _next/data requests the dev adapter preserves the query into the page URL (index.ts:3909-3910pagePathname + qs), so resolvePagesI18nRequest(...).url carries it. A miss like /_next/data/<id>/missing.json?foo=bar would emit x-nextjs-matched-path: /en/missing?foo=bar in dev, whereas prod uses the query-free matchResolvedPathname(pathname) and Next.js uses the query-free pathname (base-server.ts:2734). matchRoute strips the query internally (route-matching.ts:50) so routing is unaffected — only the header value differs. Worth a one-line .split("?")[0] on the dev path for exact parity, but it's an edge case with no test coverage either way.

Net

The PR is in good shape: the security framing is correct, the success/miss/redirect plumbing now matches Next.js, dev/prod parity is closed for the locale-prefix case, and the previously-disputed dynamic-route item is confirmed correct. Only the minor dev query-string edge case remains, and it's non-blocking.

Co-authored-by: james-elicx <james-elicx@users.noreply.github.com>
@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2239. Summary of my findings:

Review summary

I re-reviewed the full diff against a fresh vercel/next.js@canary clone and re-ran the focused suites (165 passing) plus Pages Router + features integration (654 passing); vp check clean on all changed source.

The three prior open findings are now fixed in commit ece4cdc:

  1. Config redirect soft-200 → real 307/308 — the soft-200 branch was removed from both pages-request-pipeline.ts and app-rsc-handler.ts. x-nextjs-redirect now lives only in the middleware adapter (middleware-runtime.ts), gated on trusted classification — matching Next.js exactly. Tests updated.

  2. Dev i18n data-miss now locale-prefixeddev-server.ts:555 matches Next.js base-server.ts:2734 and prod. Two new passing dev tests pin it.

  3. __isData shim typereadonly __isData?: boolean; added; no more TS2339.

Item 1 (dynamic route pattern) confirmed correct as-is — verified RouteDefinition.pathname is documented as "including dynamic placeholders" (the bracket pattern), so vinext's /blog/[slug] matches Next.js. The interpolateDynamicPath path is the minimal-mode consume branch, not the emit path.

Security model verified solid — spoofable header read gone, __isData byte-identical to Next.js and trusted-only, e2e proves forged headers don't leak, deployment-id preserved, hasMiddleware threaded consistently across all three adapters.

One minor non-blocking note: dev's miss matched-path can include a query string (e.g. /en/missing?foo=bar) where prod and Next.js use the query-free pathname. Routing is unaffected; only the header value differs. A one-line .split("?")[0] would give exact parity, but it's an untested edge case.

Net: the PR is in good shape — security framing correct, plumbing matches Next.js, parity gaps closed.

github run

@james-elicx james-elicx marked this pull request as ready for review June 21, 2026 21:20
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: Pages data-request metadata parity — re-review

I re-reviewed the full diff against the changed source, ran the focused suites (165 passing: pages-request-pipeline, app-rsc-handler, middleware-runtime), and traced the dev/prod/App-Router parity paths. The core of the PR is in good shape and the parity items raised by prior reviews are resolved. I found one real issue the prior reviews missed.

Issue: two stray node_modules symlinks committed (should be removed)

This PR adds two tracked symlinks that look like accidental git adds from a local dev environment, unrelated to the middleware change:

  • tests/e2e/app-front-redirect-issue/fixture/node_modules../../../fixtures/app-basic/node_modules
  • tests/fixtures/pages-basepath-dev/node_modules../pages-basic/node_modules

Reasons to drop them:

  1. Unrelated to the PR. pages-basepath-dev isn't referenced by any test added in this PR (no test imports/uses it), and app-front-redirect-issue is a pre-existing e2e fixture that has nothing to do with Pages data-request metadata. Both symlinks were introduced in commit 0ae6371 ("Reviewed PR #2239: fixes confirmed").
  2. Targets are gitignored. node_modules/ is in .gitignore; the symlink targets (fixtures/app-basic/node_modules, pages-basic/node_modules) won't exist in a fresh clone or CI checkout, so the symlinks dangle. They only "work" on a machine where pnpm install has already populated those dirs.
  3. No precedent. These are the only tracked node_modules symlinks in the repo — every other fixture relies on the workspace install, not a committed symlink.

Suggest git rm both and, if a shared node_modules is genuinely needed for a new fixture's dev run, rely on the workspace install / hoisting rather than committing a symlink into gitignored space.

Verified correct — no action needed

  • Security framing is right. Dropping the spoofable x-nextjs-data header read (hasDataRequestHeader) and gating exclusively on the trusted URL-derived isDataReq/isPagesDataRequest classification matches Next.js (isNextDataRequest is URL-derived). entry-templates.test.ts asserts the generated entry no longer reads the header. The e2e proves it: a forged x-nextjs-data: 1 on a non-data path returns 200, a real _next/data URL throws → 500.
  • __isData descriptor (middleware-runtime.ts:321) is byte-identical to Next.js web/adapter.ts and gated on the trusted classification. The shim type now declares readonly __isData?: boolean; (next-shims.d.ts:530), closing the TS2339 gap from the prior review.
  • Config-redirect soft-200 was correctly removed from both pages-request-pipeline.ts and app-rsc-handler.ts; config redirects now keep their real 307/308, and x-nextjs-redirect lives only in the middleware adapter — matching Next.js. New tests (pages-request-pipeline.test.ts:96, the forged-header test) lock this in.
  • dev/prod/App-Router miss parity is consistent. Pages prod uses matchResolvedPathname(pathname), App Router uses matchPathname(canonicalPathname) — both are normalizeDefaultLocalePathname, so they agree. Dev now uses `${locale ? `/${locale}` : ""}${localeStrippedUrl}` and the three new dev i18n tests (/fr/missing/fr/missing, /missing/en/missing, success → /fr/posts/[id]) pin the locale-prefixed behavior, matching prod.
  • hasMiddleware gating is equivalent across adapters. Pages gates the soft-miss on deps.hasMiddleware; App Router gates on options.runMiddleware presence — and the generated App entry only defines runMiddleware when middlewarePath exists, so the two conditions match. The miss-status gate (null | 200 | 404) matches Next.js base-server.ts.
  • Dynamic-route pattern (/blog/[slug]) emitted via patternToNextFormat(route.pattern) is correct as-is — RouteDefinition.pathname (the emit source in Next.js) is the bracket pattern, not the interpolated path. The earlier-review dispute on this is settled.

Net

The PR is functionally sound and parity-correct. The only thing worth fixing before merge is removing the two stray node_modules symlinks — they're unrelated to the change, dangle in fresh checkouts, and set a precedent no other fixture follows. Non-blocking from a behavior standpoint, but they shouldn't land.

@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2239.

Summary

I re-reviewed the full diff, ran the focused suites (165 passing), and traced dev/prod/App-Router parity. The PR is functionally sound — the security framing is correct and the parity items from prior reviews are resolved.

One real issue the prior reviews missed: the PR commits two stray node_modules symlinks unrelated to the middleware change:

  • tests/e2e/app-front-redirect-issue/fixture/node_modules
  • tests/fixtures/pages-basepath-dev/node_modules

These were added in commit 0ae6371, aren't referenced by any test in this PR, point at gitignored targets that won't exist in a fresh checkout, and are the only tracked node_modules symlinks in the entire repo. They should be git rm'd before merge.

Verified correct (no action needed): the spoofable-header removal, byte-identical __isData descriptor + new shim type, config-redirect soft-200 removal (real 307/308 restored), consistent hasMiddleware gating across all three adapters, dev/prod miss matched-path parity, and the dynamic-route bracket-pattern emit.

github run

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