mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
vinext@0.1.6
17 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f5bee1a26f |
fix(app-router): preserve unstable_catchError boundaries (#1906)
* fix(app-router): preserve unstable_catchError boundaries
* test(e2e): add Next-parity test for shouldRerenderAppPageWithGlobalError (#1906)
Add a test proving that for uncaught post-shell RSC errors (with no
client boundary), the server preserves the original HTML stream and the
client renders global-error. This is the Next.js behavior that justifies
keeping shouldRerenderAppPageWithGlobalError unconditional at false.
The server cannot detect userland client boundaries like
unstable_catchError from the route manifest, so the only safe way to
get a global-error rerender is to make a targeted error-boundary test.
The test uses a loading.tsx boundary so the error is truly post-shell,
not caught by the pre-shell error-recovery catch block.
* fix(error): restore sentinel wrapper for falsy thrown values in unstable_catchError
Restores the _CatchErrorState sentinel wrapper ({ thrownValue: unknown }) so
falsy thrown values (null, undefined, 0, "") are routed to the fallback
instead of being treated as no error. Also restores ErrorInfo.error as unknown.
Adds back unit tests for falsy values, class-component lifecycle, and
unstable_retry behavior parity with the component-rendered fallback shape.
Refs: #1906
* fix(error): align ambient ErrorInfo.error type with runtime implementation
* refactor(app-router): remove shouldRerenderAppPageWithGlobalError dead code
The function always returned false and its _options parameter and
ShouldRerenderAppPageWithGlobalErrorOptions type were vestigial.
The call site at app-page-render.ts:892-904 was therefore never taken.
Remove the function, type, import, call site, and the now-redundant
unit test, leaving the shell-level recovery path in
renderAppPageHtmlStreamWithRecovery as the sole error recovery seam.
Addresses ask-bonk review comment on #1906.
* fix(test): restore typed globalAny view and document Fallback as any intent
Addresses two non-blocking review findings from ask-bonk on PR #1906:
- Restored the typed `globalThis as unknown as { window?: unknown }`
pattern for window stubs in the unstable_catchError test section,
replacing the `(globalThis as any).window` pattern that was a type-safety
regression from the prior rewrite.
- Added an inline comment explaining why `Fallback as any` is necessary
in the class-component lifecycle test: the test probes the inner
_CatchError class directly, where the fallback function is used as a
React component type (receiving a single `{ props, errorInfo }` object)
rather than the public API signature `(props, errorInfo)`.
* chore: trigger CI
* fix(test): rename forwardedProps to props in unstable-catchError test
Addresses ask-bonk review finding on PR #1906: the test was seeding
the inner _CatchError class with the stale key name forwardedProps,
but the production wrapper now passes props (and isPagesRouter).
Since this test only exercises the App Router unstable_retry path
(which reads neither key), behavior was unaffected, but renaming
aligns the test with the current boundary shape for future readers.
* refactor(server): remove dead routeHasLocalBoundary field
Removing shouldRerenderAppPageWithGlobalError (commit
|
||
|
|
d6b9d9a1d1 |
fix(app-router): return HTTP 200 when notFound() is thrown from generateMetadata (#1864)
* fix(app-router): return HTTP 200 when notFound() is thrown from generateMetadata When generateMetadata() calls notFound(), Next.js treats the error as a suspended metadata result — the not-found UI is rendered client-side through the React NotFoundBoundary while the HTTP response stays 200. Previously, vinext was returning the boundary response with the original 404 status from renderAppPageHttpAccessFallback. The fix overrides the rendered fallback response status from 404 to 200 when specialError.fromMetadata is true for http-access-fallback errors, mirroring the existing redirect-from-metadata path which also returns 200. Fixes the failing e2e test: test/e2e/app-dir/metadata-navigation: "should support notFound in generateMetadata" * fix(app-router): gate metadata notFound 200 override on serveStreamingMetadata !== false Bonk finding: the 200 status override for notFound()/forbidden()/unauthorized() thrown from generateMetadata() was applied unconditionally, diverging from Next.js for html-limited bots (serveStreamingMetadata === false). In that path metadata blocks render synchronously and Next.js sets the real HTTP error status (app-render.tsx ~2845). Gate both branches of the http-access-fallback path (with fallback page and without) on `options.serveStreamingMetadata !== false`, symmetric with the existing redirect-from-metadata path which already carries the same gate. Adds unit tests: - fromMetadata:true + serveStreamingMetadata:false + fallback → 404 - fromMetadata:true + serveStreamingMetadata:false + no fallback → 404 |
||
|
|
bc31644649 |
fix(app-router): match streaming metadata error responses (#1794)
* fix(app-router): match streaming metadata error responses Vinext currently serializes metadata-originated redirects into a Flight payload for document requests. That response is not an HTML page, so direct browser visits cannot follow the streamed redirect and html-limited bots receive 200 instead of the blocking redirect. The dispatcher also renders HTTP access fallback head metadata from layouts only, so not-found boundary metadata is omitted when generateMetadata() throws notFound(). Use Next.js's split response model: RSC requests keep the Flight redirect digest, streaming-capable document requests receive the refresh meta tag HTML, and html-limited bots fall through to the HTTP redirect. Thread the selected HTTP access boundary module into fallback rendering so boundary metadata is resolved exactly once. * test(app-router): cover metadata RSC redirects The unit CI failure came from a strict dispatch test receiving optional fallback fields with undefined or null values. Those fields are only meaningful when a boundary module is actually selected. Build fallback option objects without absent boundary fields, preserving the existing call shape for ordinary page-level HTTP access fallbacks. Add focused coverage for fromMetadata redirects on RSC requests so metadata document refresh handling cannot accidentally consume the client navigation Flight path. * docs(app-router): update metadata redirect transport docs and drop dead boundary fallback Address review feedback on the metadata streaming error split: - Update the stale APP_PAGE_METADATA_ERROR_MARKER docblock to describe the new three-way transport split (flight digest / HTML refresh meta tag / 307). - Note the intentional minimal-stub HTML divergence from Next.js's full-document insertion in buildMetadataRedirectHtmlResponse. - Drop the redundant resolveAppPageHttpAccessBoundaryComponent fallback in renderAppPageHttpAccessFallback (boundaryModule already covers both paths). * docs(app-router): fix stale metadata redirect docs in head resolver and test reference Address remaining bonk re-review nits: - Update resolveModuleMetadata docblock in app-page-head.ts to describe the three-way transport split (it still claimed redirects ride in the flight payload even for document SSR). - Point the APP_PAGE_METADATA_ERROR_MARKER Next.js test reference at metadata-streaming, which is where the behavioural contracts are ported from. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
49d2f0966d |
feat(skip): plumb client reuse manifests through the app request path (#1717)
* feat(skip): plumb client reuse manifests through the app request path The browser never sends a client reuse manifest and the server has no path to receive one, so the static-layout skip planner (#1674) and observation foundations (#1672) have nothing to act on. Skip transport stays fully off, but the transport wiring those slices depend on is missing. Add a bounded browser producer that derives a client reuse manifest from the visible AppElements state and attaches it as X-Vinext-Client-Reuse-Manifest only on soft navigations. Parse the header into a render/send-by-default disposition and thread the parsed result through the RSC handler, the generated app entry, page dispatch, and the render lifecycle. Wire the per-layout observation tracker and segment-config classification into dispatch so the enable-transport slice can read them. No RSC payload is omitted and no response cache-control changes: the parsed manifest is carried but never authorizes a skip yet. Unproven and absent- manifest requests render and send exactly as before. Slice 3 of 4 from #1437; enable-transport follows. Refs #726 * fix(skip): address review feedback on manifest plumbing - app-page-execution: defer layout debug classification until after the observation override so the emitted reason agrees with the final flag; emit a runtime-probe reason when an observation flips a build-time static layout to dynamic. - app-page-dispatch: document classifyLayoutSegmentConfigFromModule as the request-time counterpart to the build-time classifier, noting the intentional revalidate===false divergence and the dev-mode classification. - app-render-dependency: mark registerAppElementRenderDependencies as write-only until the enable slice's consumer lands. * docs(skip): clarify layout classification comments per review - app-page-dispatch: correct the classifyLayoutSegmentConfigFromModule docblock — the build-time classifier already normalizes revalidate=false to Infinity, so both agree on that case; the real difference is scope (request-time classifies dev-mode layouts not captured at build time). - app-page-dispatch: document the merge precedence in createEffectiveLayoutClassifications — module segment config is authoritative over the build-time map. * test(skip): pin observation override flipping build-time static layout to dynamic Adds a parity assertion for the request-time behavioral change the review flagged: isLayoutObservationDynamic downgrades a build-time `static` layout to `d` (and emits a runtime-probe reason) when the observed render is unsafe for static reuse, while a safe observation keeps it static. Complements the existing children-subtree probe assertion in app-page-route-wiring.test.ts. * fix(skip): fold probe-scoped dynamic usage into layout observation The probe rewrite runs layout probes inside runWithIsolatedProbeDependencies, which opens a child unified scope and resets dynamicUsageDetected. A markDynamicUsage() during the probe that leaves no other observable trace (e.g. "use cache: private") was therefore discarded on scope exit, so the parent's consumeDynamicUsage() in the Layer-3 path returned false and the layout could classify static where the pre-PR direct probe classified dynamic. recordProbeDependencies now peeks the child scope's dynamic-usage flag before it is reset and folds it into the per-layout observation (dynamicUsageObserved), which isAppLayoutObservationUnsafeForStaticReuse treats as unsafe. This keeps the observation signal a superset of the markDynamicUsage() triggers the Layer-3 dynamicDetected path covered, so the two signals can no longer diverge into a false-static. Adds peekDynamicUsage() (non-resetting read) and a regression test. * perf(skip): defer client reuse manifest production to the cache-miss path The manifest header is excluded from VINEXT_RSC_VARY_HEADER, so it never affects the cache-busting URL or the visited-response cache lookup. Producing it up front ran createClientReuseManifestHeaderFromVisibleAppState (iterates visible layout ids, builds a cache-variant proof per static layout, then binary-searches the byte budget) on every soft navigate — including ones that resolve entirely from the visited-response cache or a prefetch and never send a request, where the work is pure waste. Move production to just before the network fetch, gated on the same `navigationKind === "navigate"` condition and computed from the captured nav-start router state, so the emitted header is unchanged on the request path while the cache-hit/prefetch-hit hot paths skip it entirely. --------- Co-authored-by: James <james@eli.cx> |
||
|
|
560f270ee6 |
fix: align app router navigation parity (#1705)
* fix: align app router navigation parity * fix(app-router): preserve canonical RSC and hard-nav semantics App Router RSC requests now keep the route pathname shape when adding _rsc. Stripping a trailing slash made trailingSlash routes fetch a non-canonical URL and let the server redirect the client-side RSC request back to the slash form. The MPA navigation scheduler also keys pending hard navigations by both href and history mode so a later replace does not reuse an earlier push closure. Redirect handling keeps the global browser bridge out of passive server rendering while retaining client-side redirect recovery. Adds focused regression coverage for trailingSlash RSC requests, same-href push-to-replace hard navigation, and client redirect guard loops. Updates existing navigation observers to the canonical route URL plus _rsc transport. * fix(app-router): stabilize action and prefetch parity * fix(app-router): reset MPA dedupe after bfcache restore A bfcache-restored App Router document can keep the scheduler's last pending hard-navigation marker after the external navigation already left the page. Reissuing the same href and history mode then entered the suspended MPA path without scheduling another location change. Expose a scheduler reset for the browser lifecycle boundary and call it on persisted pageshow events. Cover the scheduler contract so the same external navigation can run again after reset. * fix(app-router): isolate redirect bridge from passive boundaries Unhandled client redirect recovery currently lives inside every RedirectBoundary, while Pages Router params fallback can keep stale App Router params after an App-to-Pages transition. That makes passive boundaries own browser runtime wiring and lets stale app params win over live Pages Router params. Move the unhandled redirect bridge into BrowserRoot under AppRouterContext, keep RedirectBoundary passive, and prefer live Pages Router params when that compat context exists. Add an App-to-Pages navigation regression fixture. * fix: extract shared decodeRedirectError helper, rename stale test - Add decodeRedirectError() to shims/navigation.ts as the single source of truth for parsing redirect error digests - Replace three private helpers in error-boundary.tsx with shared decodeRedirectError() call - Replace inline digest parsing in AppRouterRedirectBridge with shared decodeRedirectError() call (fixes unsafe decodeURIComponent) - Rename test that claimed to test redirect() but actually tests <Link> navigation * refactor: eliminate unnecessary type assertions - isRedirectError: use in-narrowing instead of as-casts - AppRouterRedirectBridge: remove as after guard already narrows - RedirectErrorBoundary: inline digest access, use in for handled check * ci: retrigger checks * fix: emit noindex for Forbidden/Unauthorized boundaries, extract retryScrollTo helper * docs: clarify decodeRedirectError comment about defensive join |
||
|
|
fd086c4475 |
fix(app-router): apply x-edge-runtime header on all App Router response paths (#1603)
Extends #1565: the merged change gated the marker on the resolved segment config, but only covered the two `buildAppPage*Response` builders. Five other response construction paths bypassed those helpers and skipped the header for edge-runtime routes: - `app-page-cache.ts` — ISR HIT/STALE responses - `app-server-action-execution.ts` — server action RSC responses (redirect, no-revalidate, re-render) - `app-page-boundary.ts` — boundary RSC responses - `app-page-execution.ts` — 200 + flight-payload redirect responses - `app-page-stream.ts` — HTML stream responses used by the boundary path Centralizes the gating in a single `applyEdgeRuntimeHeader` helper so every site shares one source of truth for header name + condition, and threads `isEdgeRuntime` through the fallback renderer + dispatch options so each construction site receives it. Adds positive + negative coverage for each newly-covered path. |
||
|
|
fb7a2f795b |
fix(rsc): redirect returns flight payload (200) for RSC navigations (#1390)
* fix(rsc): redirect returns flight payload (200) for RSC navigations When a server component calls `redirect()` and the client makes an RSC navigation request (`.rsc` URL), encode the redirect as a React error chunk inside the flight stream and return HTTP 200 instead of a raw 307. The client's `RedirectErrorBoundary` decodes the `NEXT_REDIRECT;...` digest and performs the navigation — matching Next.js's behavior in `app-render.tsx generateDynamicFlightRenderResult`. `generateMetadata()` redirects now always return 200 with a flight payload (both RSC and SSR document requests), because metadata is suspended/streamed in Next.js — its redirects ride inside the flight body, never the HTTP status line. Page-component redirects on SSR document requests still emit a 307, unchanged. Closes #1347 * fix(rsc): preserve soft-nav and useTransition pending across flight redirect The previous commit returned a 200 + flight payload for RSC `redirect()` responses but left vinext's client-side navigation loop without a way to detect the redirect — the 200 response fell through to React's render, threw at decode time, and hit the catch path's `window.location.href` hard navigation. That tore down the page mid-transition and clobbered `useTransition`'s isPending state. Add an out-of-band header (`X-Vinext-Rsc-Redirect`) that carries the redirect target alongside the flight body. The browser-entry loop now detects the header right after the existing `resolveRscRedirectLifecycleHop` (307-following) check, drains the body, and `continue`s the loop with the new href. The same `pendingRouterState` spans both hops, so the outer transition stays pending continuously — restoring pre-1347 behavior. Also stamp the build-time `X-Vinext-Rsc-Compatibility-Id` header on the flight-redirect response. Without it, `resolveRscCompatibilityNavigationDecision` treats the response as cross-build and hard-navigates before the redirect header is read. Export `MAX_RSC_REDIRECT_DEPTH` from `app-browser-rsc-redirect` so the new header path can reuse the same hop-limit ceiling. |
||
|
|
496a0247af |
fix(app-router): emit streamed redirect and not-found meta tags (#1261)
* fix(app-router): emit streamed redirect and not-found meta tags Streaming App Router SSR currently returns a 200 once the shell commits, but redirect() or notFound() discovered inside a later Suspense boundary never emits the browser-visible meta tag that Next.js relies on. That leaves the control-flow digest only in the RSC payload until client hydration handles it. The SSR onError path now captures digest-bearing render errors and a request-local flusher injects noindex or refresh meta tags through the existing server-inserted HTML stream hook. The app page pipeline also forwards basePath into SSR so streamed redirect meta tags match early redirect responses. * fix(app-router): avoid double-prefixing basePath root redirects Streamed redirect meta tags checked basePath against the full redirect location string. Already-prefixed root URLs with a query or hash, such as /docs?from=checkout, failed the pathname-shaped basePath check and were prefixed a second time. Add a shared pathname-only basePath prefix helper and use it from both early redirect responses and streamed redirect metadata. The streamed path now splits query/hash before checking the pathname. |
||
|
|
ed11b3a576 |
refactor(server): dedupe TextDecoder stream consumption (#1083)
* refactor(server): dedupe TextDecoder stream consumption Extract the repeated `new TextDecoder()` + `ReadableStream` chunk-loop into shared helpers in `utils/text-stream.ts`: - `readStreamAsText(stream)` for the consume-to-single-string pattern (used in `app-page-cache.ts` ISR write and `pages-page-response.ts` ISR cache write). - `readStreamAsTextWithLimit(stream, maxBytes, onLimitExceeded)` for the size-limited pattern (used in `app-server-action-execution.ts` and `pages-node-compat.ts`). Each caller passes its own throwing callback so error types (`Error` vs `PagesBodyParseError`) and status codes are preserved. Sites with additional load-bearing logic intentionally left inline: - `rsc-stream-hints.ts` (line-buffered TransformStream with carry) - `app-ssr-stream.ts` (raw-byte accumulator + flight hint fix; HTML flush transform with `fixPreloadAs`) - `shims/fetch-cache.ts` (size-limited cache-key body canonicalisation with mixed string/Uint8Array stream and tee handling) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(server): address review feedback from #1083 - Dedupe `readAppPageTextStream` in `app-page-execution.ts` (called once from `app-page-dispatch.ts`); replace with `readStreamAsText` and remove the now-unused export. The deduped helper additionally releases the reader lock in `finally`, which the inline copy did not — a robustness improvement on a wholly-consumed stream. - Update `tests/app-page-execution.test.ts` to use `readStreamAsText`. - Mirror the `try/finally { reader.releaseLock() }` pattern in `readStreamAsTextWithLimit` for consistency with `readStreamAsText`. The happy path implicitly releases when the stream completes; the over-limit path cancels which also releases. The explicit release prevents lock leaks if a future caller catches the limit error and reuses the reader handle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7b41e950bd |
fix(app-page): align redirect()/notFound() handling under loading.tsx with Next.js (#1057)
* fix(app-page-probe): always await page promise so redirect()/notFound() under loading.tsx return 307/404 The probe used to fire-and-forget the page promise when a route-level loading.tsx was present (`awaitAsyncResult: !hasLoadingBoundary`). For async pages that throw `redirect()` or `notFound()`, that swallowed the rejection during probe; the RSC stream then re-ran the page under the route Suspense boundary, where React converted the throw into a "Switched to client rendering" error serialized into a 200 HTML body instead of a clean 307/404. Awaiting the page in the probe surfaces the special error early, parity with the no-loading-boundary path. The cost is that the outer page function runs serially with the RSC render rather than in parallel — in practice most async work in `loading.tsx`-shielded routes happens in nested Suspense children, not in the outer page function body. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: capture special-error digests post-render instead of awaiting probe Replaces the previous "always await page in probe" approach with a post-render swap that mirrors Next.js's mechanism in app-render.tsx: the page render's onError captures NEXT_REDIRECT / NEXT_HTTP_ERROR_FALLBACK digests; after the HTML stream drains, the lifecycle inspects the tracker and swaps a 307/404 in place of the streamed "Switched to client rendering" body. This avoids the 2× page invocation cost of awaiting in the probe (the previous fix called the page once during probe and again during RSC render, serially). Now the page runs once, inside the RSC render, and the route-level Suspense boundary (loading.tsx) traps the throw as designed. The buffered stream still contains the same final HTML for non-redirecting pages. Trade-off matches Next.js: loading.tsx fallback visibility is sacrificed for routes that redirect (you cannot both flush the loading state and reverse course to a 307). Non-redirecting routes with loading.tsx now buffer the full response rather than streaming progressively — same trade-off Next.js makes with its prerender-based pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert "refactor: capture special-error digests post-render instead of awaiting probe" This reverts commit 179b20f88033f3731677a91faeb022389247223e. * refactor: align with Next.js — skip page probe + post-shell race for special errors Replaces the always-await fix with a Next.js-aligned pipeline: - Skip the page probe entirely when a route-level loading.tsx is present. The page now runs once, inside the RSC render, instead of twice serially. - Capture NEXT_REDIRECT / NEXT_HTTP_ERROR_FALLBACK digests in the rscErrorTracker via React's onError callback (previously dropped). - After the SSR shell is ready but before bytes are flushed, race the captured digest against a 50 ms swap window. A digest that fires within the window — sync throws and short-async like ~10 ms auth checks — is converted to a 307/404. Late rejections fall through to the streamed body, matching Next.js's "until-first-byte-is-flushed" semantic. Trade-offs vs. the previous always-await fix: - Page invoked once instead of twice (no more 2× latency for async outer page functions). - TTFB for loading.tsx routes goes from ~outer-page-time to ~50 ms. - loading.tsx fallback is visible immediately after the swap window closes — the OpenNext-compat E2E that broke under the buffer-everything approach passes. Slow async redirects (>50 ms) still leak as serialized stream errors. Next.js handles that case by injecting <meta http-equiv="refresh"> mid-stream; doing the same in vinext is a separate, larger change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: drop 50ms swap-window — deterministic post-shell digest check Removes the timer-race hack and replaces it with the same shape Next.js uses (`app-render.tsx:4293`): after the SSR shell promise resolves, inspect the rscErrorTracker for a captured NEXT_REDIRECT/NEXT_HTTP_ERROR_FALLBACK digest and swap the response to a 307/404 if one is present. Why this works without a timer: the SSR pipeline naturally runs many microtask awaits between the page Promise rejecting (in onError) and the lifecycle reading the tracker. Throws that resolve in microtasks during shell rendering are deterministically captured. Throws that require macrotask boundaries (real I/O, setTimeout) are NOT caught and fall through to the streamed body — exactly the same trade-off Next.js makes (the digest survives in the Flight payload for the client router to consume). Updates the regression fixture to use the deterministic case (sync throw inside an async page) instead of the pre-fix repro's setTimeout trick. The setTimeout case can't be deterministically caught without either a timer (rejected as a hack) or React-internals patches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: regression for permanentRedirect() under loading.tsx returning 308 Adds a fixture and integration test mirroring the redirect()-with- loading.tsx case but using permanentRedirect (308). Confirms the post-shell digest swap honors the statusCode field from the NEXT_REDIRECT digest rather than coercing to the 307 default — closes a flagged gap from the Next.js parity audit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: regression for forbidden()/unauthorized() under loading.tsx Adds fixtures and integration tests verifying the post-shell digest swap correctly preserves the status code from NEXT_HTTP_ERROR_FALLBACK digests: - forbidden() under loading.tsx → 403 with root forbidden.tsx body - unauthorized() under loading.tsx → 401 with root unauthorized.tsx body Confirms resolveAppPageSpecialError reads `parseInt(parts[1], 10)` from the digest rather than coercing to 404 — closes the parity-audit's "forbidden/unauthorized status coercion" gap (which turned out to already be handled correctly; this just locks it in). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(app-page): apply basePath to redirect Location header Mirrors Next.js's `addPathPrefix(getURLFromRedirectError(err), basePath)` in app-render.tsx — `redirect("/about")` from a page mounted under basePath "/blog" now produces `Location: /blog/about` instead of the raw "/about". - Adds `basePath` to BuildAppPageSpecialErrorResponseOptions and a small helper that resolves the redirect target against the request URL and prefixes app-internal absolute paths. - External redirects (different origin) and targets that already start with the basePath are passed through unchanged. - Threads the entry-generated `__basePath` through dispatch and into both the page and layout special-error response builders. - Closes a parity gap flagged in the Next.js audit; covered by 5 new unit assertions exercising internal / external / already-prefixed / unconfigured / root-target cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(app-page): preserve cookies().set() on redirect responses Mirrors Next.js's `appendMutableCookies(headers, requestStore.mutableCookies)` in app-render.tsx. Auth flows that do cookies().set("session", "..."); redirect("/dashboard"); must preserve the Set-Cookie on the 307, otherwise the redirected request lands without the just-issued session and the user bounces back to login. - Adds an optional `getAndClearPendingCookies` to BuildAppPageSpecialErrorResponseOptions that, when set, drains pending Set-Cookie values accumulated during the render and appends them to the redirect response. - Wires `getAndClearPendingCookies` from `vinext/shims/headers` into both the page and layout special-error response builders in dispatch. - Only applied to redirect responses to match Next.js — http-access-fallback responses leave cookies to the rendered boundary. - Closes a parity gap from the audit; covered by a new unit test exercising redirect-with-cookies, redirect-without-cookies, and the http-access-fallback-cookies-not-bled cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: regression for notFound() under loading.tsx Adds a fixture and integration test covering the most common loading-boundary special-error case in real apps: a dynamic detail page with a loading state that calls notFound() when the record is missing. Distinct from the forbidden()/unauthorized() coverage — notFound() throws the bare "NEXT_NOT_FOUND" digest (not "NEXT_HTTP_ERROR_FALLBACK;404"), which takes a separate branch in resolveAppPageSpecialError. This locks in that the post-shell digest swap handles both branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9d5d1ce50d |
fix(app-router): validate RSC cache-busting params (#991)
* fix(app-router): validate RSC cache-busting params App Router RSC responses could vary by request headers while client fetches and server validation still relied on Vary-aware caches. CDNs that key only by URL could reuse a mounted-slot or interception-context RSC payload for a different request. Next.js validates a cache-busting _rsc search param against the RSC request headers and redirects malformed requests to the canonical URL. Vinext now applies the same invariant to its .rsc request model, centralizes the RSC Vary header set in normal server modules, and de-duplicates middleware Vary additions. Tests cover missing and stale _rsc values, variant-header hashes, generated-entry delegation, browser RSC request markers, response Vary propagation, and App Router RSC integration. * fix(app-router): hide RSC cache tokens from navigation URLs * fix(app-router): hide RSC cache token from searchParams * fix(app-router): address RSC cache-busting review * fix(app-router): preserve HTML redirect destination for direct .rsc navigations Config redirects from next.config.js were unconditionally converted to .rsc destinations for any request whose URL ended in .rsc. This broke direct browser navigations to .rsc URLs (e.g. a bookmark or external link) because the browser was redirected to an RSC payload it could not render. Client-side RSC fetches always send the RSC: 1 header, so restrict the .rsc destination conversion to requests that carry that header. Direct browser hits without the header continue to redirect to the canonical HTML URL, matching the pre-PR behaviour for config redirects. |
||
|
|
d45c5905f3 |
perf: fuse RSC stream tees reducing allocs from 3 to 2 (#984)
* perf: fuse RSC stream tees reducing allocs from 3 to 2 ISR cache path teed the RSC stream twice (once for capture, once for embed). Fuse into single tee: sideStream serves both embed transform and raw byte accumulation via createRscEmbedTransform. Tee 1 (capture) and Tee 2 (embed) now share one tee. Tee 3 (HTML cache) remains separate — it operates on post-SSR HTML with a conditional write decision. Fixes #981 * fix: address PR review feedback for fused RSC stream tees (#981) - Fix AppPageSsrHandler type to include sideStream and capturedRscDataRef - Clear rawChunks after getRawBuffer() to reduce peak memory usage - Remove misleading no-op null assignment in handleSsr else branch - Guard await null in ISR cache write to prevent undefined in cache * fix: repair CSP nonce handling and update snapshots for #981 - Fix renderAppPageHtmlStream to always pass ssrOptions (including scriptNonce) to handleSsr, instead of conditionally when sideStream is defined. This fixes CSP nonce regressions in dev mode. - Update 6 snapshot occurrences in entry-templates.test.ts.snap to match the updated __revalCapturedRscRef.value access pattern with null safety. Fixes CI failures for the fuse RSC stream tees PR. |
||
|
|
67a929bf25 |
Preserve middleware headers on app boundary responses (#908)
* Preserve middleware headers on app boundary responses * test: stabilize app router redirect e2e |
||
|
|
34ffb948c8 |
chore(debug): classification reasons sidecar behind VINEXT_DEBUG_CLASSIFICATION [6/6] (#843)
* chore(debug): add classification reasons sidecar behind VINEXT_DEBUG_CLASSIFICATION
Operators tracing why a layout was flagged static or dynamic can opt in
with VINEXT_DEBUG_CLASSIFICATION=1 at build time. When active, the
plugin patches a second stub (__VINEXT_CLASS_REASONS) with a dispatch
table that returns per-layout ClassificationReason structures. The
runtime probe loop emits a debug line per layout when
debugClassification is wired, and the hot path remains allocation-free
when debug is off because the stub returns null.
The reasons machinery (types, report.ts tagged results, and
buildReasonsReplacement) already shipped in the previous PR; this PR
just enables the emission path and wires the debug consumer into the
runtime probe.
* test(debug): cover runtime-probe error path and Layer 2 reasons dispatch
The runtime-probe catch branch in `probeAppPageLayouts` emits
`{ layer: "runtime-probe", outcome: "dynamic", error: "..." }` when the
isolated dynamic scope throws. That emission had no coverage, so a
regression that stopped including the `error` field, or that skipped the
debug callback entirely on the error path, would not have tripped any
test.
The reasons sidecar integration test also only asserted the Layer 1
(`segment-config`) reason on the nested `force-dynamic` layout. Layer 2
(`module-graph`) on the pure-JSX root layout was exercised in the build
but never read, so a regression that stopped serializing the `result`
field or the `module-graph` discriminator for Layer 2 would slip through.
Extends the existing debug-on describe block to read reasons for layout
index 0 on `/force-dyn` and assert the `module-graph` + `result: "static"`
pair, keeping the structural `ReasonShape` so the test still survives
the #863 codegen reshape.
Also removes a pre-existing `as` on the narrowed Map return in favour of
a proper `in`-narrowing type predicate.
|
||
|
|
9791a63381 |
feat(app-router): emit per-layout flags in the RSC payload [2/6] (#839)
* feat(app-router): emit per-layout flags in the RSC payload Wire runtime layout classification through renderAppPageLifecycle and attach the resulting flags as __layoutFlags in the outgoing RSC payload via buildOutgoingAppPayload. Payload-shape helpers (withLayoutFlags, isAppElementsRecord, buildOutgoingAppPayload, AppOutgoingElements) live alongside the existing readAppElementsMetadata so the write and read boundaries sit next to each other. * fix(app-elements): widen isAppElementsRecord narrowing and drop redundant clone - isAppElementsRecord narrowed to Record<string, ReactNode>, but the outgoing payload legitimately carries heterogeneous values (ReactNode for the rendered tree plus LayoutFlags under __layoutFlags). Widen the predicate to Readonly<Record<string, unknown>> so the return type matches what the runtime check actually proves. - buildOutgoingAppPayload spread input.element into a new object before passing it to withLayoutFlags, which immediately spreads again. Drop the outer copy; withLayoutFlags already guarantees immutability. * fix(app-router): clear leaked dynamic usage on layout probe errors |
||
|
|
18237955fa |
feat: static/dynamic layout detection for skip-header optimization (#767)
* feat: add classifyLayoutSegmentConfig for layout segment config detection
Reads `export const dynamic` and `export const revalidate` from layout
source files to classify them as static or dynamic. Unlike page
classification, positive revalidate values return null (ISR is a page
concept), deferring to module graph analysis for layout skip decisions.
* feat: add module graph layout classification (Layer 2)
BFS traversal of each layout's dependency tree via Vite's module graph.
If no transitive dynamic shim import (headers, cache, server) is found,
the layout is provably static. Otherwise it needs a runtime probe.
classifyAllRouteLayouts combines Layer 1 (segment config, from prior
commit) with Layer 2 (module graph), deduplicating shared layouts.
* feat: per-layout dynamic detection in probe phase (Layer 3)
Extends probeAppPageLayouts to return per-layout flags ("s"/"d")
alongside the existing Response. Three paths per layout:
- Build-time classified: pass flag through, still probe for errors
- Needs probe: run with isolated dynamic scope, detect usage
- No classification: original behavior (backward compat)
probeAppPageBeforeRender propagates layoutFlags through the result.
renderAppPageLifecycle updated to destructure the new return type.
* feat: add __layoutFlags payload metadata and thread through router state
Adds APP_LAYOUT_FLAGS_KEY to the RSC payload metadata, carrying
per-layout static/dynamic flags ("s"/"d"). readAppElementsMetadata
now parses layoutFlags with a type predicate guard.
AppRouterState and AppRouterAction carry layoutFlags. Navigate merges
flags (preserving previously-seen layouts), replace replaces them.
All dispatchBrowserTree call sites updated to pass layoutFlags.
* refactor: group classification options into single LayoutClassificationOptions type
The three optional fields (buildTimeClassifications, getLayoutId,
runWithIsolatedDynamicScope) had an all-or-nothing invariant enforced
only at runtime. Grouping them into a single optional `classification`
object makes the constraint type-safe — you either provide the full
classification context or nothing.
Also deduplicates the LayoutFlags type: canonical definition lives in
app-elements.ts, re-exported from app-page-execution.ts.
* fix: default to dynamic flag when layout probe throws non-special error
When runWithIsolatedDynamicScope throws and the error is non-special
(onLayoutError returns null), the layout was silently omitted from
layoutFlags. Now conservatively defaults to "d" — if probing failed,
the layout cannot be proven static.
* refactor: replace parallel arrays with LayoutEntry struct, tighten names
- Introduce LayoutEntry (moduleId, treePosition, segmentConfig) in
layout-classification.ts, collapsing the three parallel arrays
(layouts/layoutTreePositions/layoutSegmentConfigs) in
RouteForClassification into a single struct. Switches the inner loop
to for...of, removing the ?? 0 fallback that masked index mismatches.
- Update all five classifyAllRouteLayouts test fixtures to the struct shape.
- Add a JSDoc on LayoutFlags clarifying that "s" = static, "d" = dynamic.
- Tighten the buildTimeClassifications comment to say "keyed by layout index".
https://claude.ai/code/session_01MDJzCNao3dX9tmmVtSqsw5
* style: fix prettier formatting
https://claude.ai/code/session_01MDJzCNao3dX9tmmVtSqsw5
* Revert "style: fix prettier formatting"
This reverts commit
|
||
|
|
c0878d7056 | refactor: extract app page execution helpers (#630) |