* chore: migrate to vite plus
* Disable typeAware and typeCheck
* Update CI
* Fix CI
* Fix test
* Clean
* Run test with vp
* Try revert
* react: false In test
* Fix test
* Revert "Try revert"
This reverts commit 009da10473.
* Update
* Update
* Try revert ci changes
* revert
* Run vp migrate
* Disable typeAware and typeCheck for now
* Better resolve for test
* Use vp dev instead of vite
* Update expect
* Fix NormalizeManifestModuleId
* Try increase timeout
* Update to use vp
* Try new check
* Bring back npx vp
* Migrate CI
* Make next-intl resolvable
* Update
* Update
* Update
* test: add concurrent ssr isolation fixture pages for pages router
* test: add pages router dev concurrency isolation tests
* fix: register head/router als accessors in ssr module graph
* test: add prod concurrency isolation tests
* docs: add als architecture and per call scope docs
* fix: move als accessor registration to one time setup in createSSRHandler
* test: add assertion messages and explicit guards for ci debugging
* test: use lookahead in extractMetaContent for attribute order independence
* test: add comment explaining prod head test limitations
* docs: clarify pr #450 wording in als architecture doc
* test: add random delay in fixture getServerSideProps to increase interleaving
* test: align concurrent-router query type with ParsedUrlQuery
* test: use shared createIsolatedFixture helper in prod concurrency tests
* docs: clarify ssrLoadModule guidance in als architecture doc
discoverInheritedParallelSlots iterates from root to leaf, but used
a `has()` guard that let the first (farthest) ancestor win. Remove
the guard so later (closer) ancestors overwrite earlier ones.
The ISR background regeneration callback in the generated App Router entry
was passing the triggering user's url.searchParams into setNavigationContext()
and buildPageElement(). This meant cached content reflected one user's query
params and got served to all subsequent users — a data leak.
Headers were already correctly emptied (new Headers()), but searchParams were
missed. Now both use new URLSearchParams() to produce clean cached content.
* feat: add ISR caching for App Router route handlers
Route handlers with `export const revalidate = N` now get server-side
ISR caching (MISS/HIT/STALE with background regeneration), matching
the existing page ISR behavior. Previously only Cache-Control headers
were emitted — the handler re-executed on every request.
- Add `__isrRouteKey` helper (suffix "route") for cache key construction
- ISR cache READ before handler execution (HIT skips handler entirely)
- ISR cache WRITE on MISS via `response.clone()` + `waitUntil`
- STALE serves stale data and triggers background regen with clean
context (synthetic request, empty headers/cookies)
- Guards: production-only, GET/HEAD only, skips when handler is dynamic
(uses headers()/cookies()), skips when handler sets own Cache-Control,
filters Infinity from revalidateSeconds
- Reuses existing CachedRouteValue (kind: "APP_ROUTE") and KV serialization
* chore: update entry-templates snapshots for route handler ISR
* test: add auto-HEAD ISR cache test for route handlers
* fix: review findings — force-dynamic guard, consistent header filtering in cache writes
* test: poll for ISR background regen instead of fixed sleep
Replace fixed 1000ms sleep with a polling loop (up to 5s) to wait
for the background regen to complete. The fixed sleep was flaky
because regen timing varies with system load. Also bumps the stale
wait from 1500→2000ms for slow CI.
* fix: capture request.url before regen closure to avoid pinning Request
Hoist request.url into a local const (__revalUrl) before the
background regeneration closure. This lets the original Request
object (with its body stream and headers) be GC'd sooner on
memory-constrained Workers isolates.
* fix: address remaining review findings
- Hoist url.searchParams into __revalSearchParams before the regen
closure so the outer URL object can be GC'd during background regen
(same class of fix as the request.url hoisting)
- Add test for force-dynamic + revalidate route handler guard
- Add comment noting order-dependent test cache state
* fix: add missing set/delete/clear/size/toString to RequestCookies
The RequestCookies class in server.ts (used by NextRequest.cookies) was
missing mutation methods that @edge-runtime/cookies provides. Middleware
that calls cookies.set() or cookies.delete() to forward modified cookies
downstream would fail at runtime.
- Add set(), delete(), clear(), size, toString() methods
- Cache parsed map in constructor instead of re-parsing on every call
- Encode values with encodeURIComponent when syncing back to Cookie header
- delete() accepts string | string[] and returns boolean | boolean[]
matching @edge-runtime/cookies API
* refactor: DRY up RequestCookies clear/toString/syncHeader
Address review feedback:
- clear() delegates to _syncHeader() instead of inlining header deletion
- Extract _serialize() helper shared by toString() and _syncHeader()
Libraries that import `next/config.js` or `next/amp.js` (with the
explicit .js extension) failed to resolve to vinext shims. Every other
public next/* entrypoint already had both variants.
Adds a regression test that verifies all top-level next/* aliases have
a corresponding .js variant, preventing future omissions.
The App Router generated entry called mwFetchEvent.drainWaitUntil() but
discarded the return value, so middleware background promises registered
via event.waitUntil() were abandoned when the response was sent on
Workers. Register the drained promise with the execution context,
matching the Pages Router entry pattern.
* refactor: consolidate 5 nested ALS scopes into unified request context
Every App Router request previously ran through 5-6 nested
AsyncLocalStorage.run() calls (headers, navigation, cache-state,
private-cache, fetch-cache, plus optional execution-context). Each
ALS scope push/pop has measurable cost on Workers isolates.
Introduce a single UnifiedRequestContext that holds all per-request
state in one flat object, backed by one ALS instance. Each shim module
checks isInsideUnifiedScope() first and reads its sub-fields from the
unified store, falling back to its own standalone ALS when outside
(SSR environment, Pages Router, tests).
- Create unified-request-context.ts with createRequestContext(),
runWithRequestContext(), getRequestContext(), isInsideUnifiedScope()
- Rewire _getState() in headers, navigation-state, cache, cache-runtime,
fetch-cache, and request-context to dual-path (unified first, own ALS
fallback)
- Make each runWith*() a no-op inside unified scope
- Replace 5-deep nesting in app-rsc-entry.ts (main handler + ISR regen)
with single _runWithUnifiedCtx() call
- Export ensureFetchPatch() from fetch-cache for standalone patch install
- Add 17 unit tests for unified context (isolation, nesting, concurrency)
* test: update entry-templates snapshots for unified ALS context
Snapshots were stale after the unified request context refactor changed
the generated entry code (new imports, removed 5-deep nesting).
* test: update app router waitUntil assertion
* refactor: unify request ALS across router flows
* fix: address unified request context review feedback
* fix: address unified ALS review feedback
* fix: address remaining unified ALS review feedback
* fix: wrap Pages Router ISR regeneration in unified context
Ensures patched fetch and ALS-based context are available during
background regeneration, matching App Router behavior.
- Creates fresh unified context with execution context for regen
- Calls ensureFetchPatch() to enable cache tagging
- Adds test verifying _runWithUnifiedCtx and ensureFetchPatch presence
- Updates snapshot
Part of Phase 1.5: minimal Pages Router fix for ISR parity.
* Fix dev ISR unified context parity
* fix: address unified ALS review followups
* Fix pages router unified execution context seeding
* Fix Pages Router ISR rerender state isolation
* fix: address unified ALS review feedback
* chore: upgrade vitest to 4.1
* fix: clear console.warn spy state before asserting call counts
Vitest 4 changed vi.spyOn to return the same mock when called on an
already-intercepted function. This means console.warn call records from
earlier tests accumulate in the spy, causing toHaveBeenCalledTimes(2) to
see 4 calls instead of 2.
Adding mockClear() after each spy creation resets the call count so the
assertions only reflect calls made within the test itself.
* refactor: eliminate as any / as unknown as type assertions
- shims/fetch-cache.ts: introduce ExtendedRequestInit type to replace
all (init as any)._ogBody casts and body duck-type-check casts
- shims/navigation.ts: add VinextNavigationError subclass with typed
digest field, removing all (error as any).digest writes; widen
_nativeReplaceState to | null and add null guard
- shims/constants.ts: remove (process.features as any) cast; fixed via
NodeJS.ProcessFeatures augmentation in global.d.ts
- shims/router.ts: remove listeners.get()! non-null assertion
- check.ts: remove pagesDir!, appDirPath!, importUsage.get()! non-null
assertions; use direct null-checks instead
- entries/pages-server-entry.ts: remove appFilePath!, docFilePath! non-
null assertions; use direct null-checks instead
- index.ts: drop 'unknown' step in two Readable.toWeb / proxyResponse.body
ReadableStream casts
- global.d.ts: augment NodeJS.ProcessFeatures with optional typescript
field
- cloudflare/kv-cache-handler.ts: add SerializedIncrementalCacheValue
family of types; update serializeForJSON / restoreArrayBuffers to use
them, eliminating ~10 as any casts
* fix: restore typeof guards in restoreArrayBuffers to preserve original behaviour
The refactor dropped the 'typeof value.rscData === "string"' style guards
from restoreArrayBuffers, causing it to call safeBase64ToArrayBuffer on
absent fields (undefined) and return null for entries that only have
{ kind } set — breaking the schema validation test.
Restore the typeof-string guards so the function only decodes fields that
are actually present, matching the original behaviour exactly.
Also make body/buffer optional in SerializedCachedRouteValue /
SerializedCachedImageValue to reflect runtime reality.
* fix: handle URL objects in metadata resolveUrl
The Next.js Metadata API types URL fields as `string | URL` (e.g.
`metadataBase`, `openGraph.url`, icon/image URLs). The internal
`resolveUrl()` function in `MetadataHead` only accepted `string`,
so passing a `URL` object (which is valid per Next.js types) crashed
with `url.startsWith is not a function`.
Add type coercion at the top of `resolveUrl()` to convert `URL`
objects to strings before processing.
Reproduction:
1. Set `metadataBase: new URL("https://example.com")` in a layout
2. Set `openGraph: { url: "/page" }` in a page's metadata
3. `resolveUrl` receives the URL object, calls `.startsWith()` on it
4. Crash: `url.startsWith is not a function`
With this fix, `URL` objects are coerced to strings via `.toString()`.
* fix: update Metadata types and rendering to accept string | URL fields
Address bonk review feedback:
- Add test covering URL object input for alternates.canonical and
openGraph.url to lock in the fix against regressions
- Update Metadata interface to type URL fields as string | URL,
matching Next.js types (openGraph.url/images/videos/audio,
twitter.images, icons, manifest, alternates.canonical/languages/
media/types)
- Add overload to resolveUrl() so it returns string when called with
a non-undefined argument, eliminating string | URL fallback issues
- Fix image/icon array normalisation to handle bare URL objects
alongside strings and descriptor objects
* fmt
* fix: remove dead-code ?? fallbacks and unnecessary test casts
Address bonk re-review feedback:
- Remove 'as unknown as string' casts in the URL-object test since the
Metadata interface now types those fields as string | URL
- Remove all 17 remaining 'resolveUrl(x) ?? x' fallbacks; resolveUrl
is overloaded to return string for any truthy input so these never
fired, and with string | URL fields the fallback value would have
passed a URL object directly to content=/href= attributes
* fmt
---------
Co-authored-by: James <james@eli.cx>
* fix: serve metadata routes in dynamic segments (opengraph-image, icon, etc.)
Metadata routes like opengraph-image.tsx inside dynamic segments
(e.g., app/blog/[slug]/opengraph-image.tsx) returned 404 because the
runtime matching used strict equality against the servedUrl which
contains literal bracket notation ([slug]).
Two fixes:
- Generate patternParts for metadata routes with dynamic segments so
the runtime uses matchPattern() instead of strict equality
- Pass extracted params to the metadata function via makeThenableParams()
matching Next.js behavior where dynamic metadata routes receive { params }
* fix: address review — scope patternParts to dynamic routes, fix indentation
- Only generate patternParts for dynamic metadata routes (static files
don't use params)
- Add "keep in sync" comment for segment-to-pattern conversion
- Fix mis-indented try/catch braces in generated static metadata code
sitemapToXml() interpolated user-supplied values (URLs, video titles,
descriptions, uploader names) directly into XML without escaping. URLs
containing & (extremely common with query params) produced invalid XML
that search engines reject silently.
Add escapeXml() helper that handles all five XML special characters
(&, <, >, ", ') and apply it to every user-supplied value in the
sitemap serializer.
This intentionally diverges from Next.js's resolveSitemap() which also
raw-interpolates, producing malformed XML.
Adding benchmarks/* to the workspace in the vite v8 bump caused pnpm to
hoist next out of benchmarks/nextjs/node_modules into the monorepo root.
Turbopack then couldn't find next/package.json from the project directory,
failing with a workspace root inference error.
Fix by listing benchmarks/vinext and benchmarks/vinext-rolldown explicitly
instead of using the glob, leaving benchmarks/nextjs isolated with its own
node_modules.
Also remove the eslint key from next.config.ts — it was removed from the
NextConfig type in Next.js 16 and caused an 'Unrecognized key' warning.
* fix: preserve query params on middleware rewrite in App Router
The middleware rewrite handler in app-rsc-entry.ts only extracted
pathname from the rewrite URL, silently discarding the query string.
All downstream code (searchParams props, useSearchParams(), navigation
context, ISR cache keys) continued using the original request's search
params instead of the rewrite destination's.
Fix: mutate url.search after extracting the rewrite pathname so all
~30 downstream url.searchParams references automatically pick up the
rewrite's query string. This matches the Pages Router's existing
behavior in server/middleware.ts which already preserves pathname+search.
* test: update entry-templates snapshot
pendingRegenerations and revalidateDurations were plain module-level
Maps, giving each Vite environment (RSC, SSR) its own copy. This meant
dedup didn't work across environments — both could trigger regeneration
for the same key simultaneously.
Use Symbol.for() on globalThis to share a single Map instance, matching
the pattern already used in fetch-cache.ts.
* fix: dedup stale background refetches in fetch cache
Every concurrent request hitting a stale cache entry was independently
firing its own originalFetch(), creating a thundering herd on popular
endpoints. Add a per-cache-key dedup map (pendingRefetches) shared
across RSC/SSR environments via Symbol.for(), matching the existing
ISR regeneration dedup pattern in isr-cache.ts.
Also adds a 60s timeout safety net that force-cleans dedup entries
when upstream fetches hang, with identity checks in both .finally()
and setTimeout to prevent slot-ownership races.
* test: add fetch cache stale dedup concurrency tests
- Concurrent stale hits trigger only one background refetch
- Completed refetches allow new ones (lifecycle test)
- Failed refetches clean up dedup entry (error-path test)
- Hung fetches are force-cleaned after timeout
- Late-settling hung fetch does not evict replacement refetch
- _resetPendingRefetches() called in beforeEach for test isolation
* Update packages/vinext/src/shims/fetch-cache.ts
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
* fix: repair broken setTimeout structure from review suggestion
The applied suggestion duplicated the setTimeout block and broke
brace nesting. Fix the structure and also clear the timeout in
.finally() so completed refetches don't leave dangling timers.
* fix: wrap fake timer tests in try/finally for safety
If a test throws before vi.useRealTimers(), subsequent tests would
run under fake timers and break. Wrap both fake timer tests in
try/finally to guarantee cleanup.
* fix: guard background revalidation against caching error responses
The stale-while-revalidate background refetch unconditionally cached
whatever response came back, including 500s. A transient server error
would permanently replace good cached data. Add a `response.ok` guard
matching the existing cache-miss path.
* fix: tighten cache guard to status === 200
Per review feedback, narrow the cache guard from `response.ok` (2xx)
to `response.status === 200` in both the background revalidation path
and the cache-miss path. Non-200 success codes (201, 204, 206) are
not meaningful to cache in the fetch cache context.
---------
Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
The i18n locale context (locale, defaultLocale, domainLocales,
hostname) was stored on bare globalThis properties, which are
shared across concurrent requests. Under concurrent traffic
(Cloudflare Workers or Node.js production server), request A's
locale could be overwritten by request B before A finishes
rendering, causing Link components to produce incorrect
locale-prefixed URLs.
Introduce ALS-backed i18n state following the existing pattern
used by router-state.ts and head-state.ts:
- i18n-context.ts: bridge module with fallback globalThis
accessors and a registration API for ALS-backed overrides
- i18n-state.ts: server-only module that registers ALS-backed
accessors on import, providing per-request isolation
link.tsx now reads locale context via getI18nContext() instead
of bare globalThis, dev-server.ts sets context via ssrLoadModule
for the SSR environment, and the production entry wraps
rendering in runWithI18nState().
* fix: MemoryCacheHandler revalidate:0 creates immediately-stale entries
MemoryCacheHandler.set had an inconsistent guard on data.revalidate:
the ctx path checked `revalidate > 0` but the data path only checked
`typeof data.revalidate === "number"`, allowing revalidate:0 to set
revalidateAt = Date.now() — making entries immediately stale.
This was inconsistent with KVCacheHandler which already had the > 0
guard on both paths. The two handlers would produce different staleness
behavior for the same input: Memory → immediately stale, KV → no TTL.
Add the missing `> 0` guard to align both code paths and both handlers.
* fix: revalidate:0 should skip cache storage entirely
Both MemoryCacheHandler and KVCacheHandler had inconsistent handling of
revalidate:0. In Next.js, revalidate:0 means "don't cache." But the
handlers would still store entries — Memory made them immediately stale
(data path lacked > 0 guard), KV cached them forever (both paths
rejected 0, so revalidateAt=null meant no expiry).
Resolve effective revalidate from both ctx and data paths (data
overrides ctx), and early-return without storing when it's 0. Both
handlers now use identical logic.
Fix existing test data that incidentally used data.revalidate:0 in
tests meant to exercise stale-while-revalidate and tag invalidation.
computeInterceptTarget() used path.dirname() to climb parent directories,
but Next.js defines interception levels in terms of route segments. Route
groups like (marketing) are filesystem directories but invisible in the
URL, so path.dirname() would waste a climb on them.
Now walks backward through segments, counting only visible ones (skipping
route groups, parallel slots, and ".").
Also consolidates three copies of invisible-segment detection into a
single isInvisibleSegment() helper.
* fix: support onLoadingComplete in modern next/image shim
Add the deprecated-but-still-supported onLoadingComplete prop to the
modern next/image component, matching Next.js behavior where the
callback receives the underlying HTMLImageElement on load.
- Add onLoadingComplete to ImageProps interface and type declarations
- Wire handleLoad into all render paths (local, custom loader, remote)
- Destructure onLoad/onLoadingComplete in getImageProps to prevent leak
* fix: correct callback order and add remote URL test
- Swap onLoad/onLoadingComplete invocation order to match Next.js
(onLoad fires first, then onLoadingComplete)
- Fix stale comment that described legacy { naturalWidth, naturalHeight }
- Add remote URL (UnpicImage) SSR test for full path coverage
* fix: parse :param(constraint) in middleware matchers
The middleware matcher compiler in compileMatcherPattern() treated any
pattern containing ( as a raw regex, so /blog/:id(\d+) was compiled as
the literal regex ^/blog/:id(\d+)$ — matching "/blog/:id123" instead
of "/blog/123".
Port the constraint-aware extractConstraint() approach from
config-matchers.ts: when a ( follows a :param name, extract it as an
inline regex constraint and emit a proper capture group. Patterns
without :param( syntax (e.g. /((?!api|_next).*)) still take the raw
regex path.
Also supports :param(constraint)? for optional constrained segments
(e.g. /:locale(en|es|fr)?/about matches both /about and /en/about).
Mirrored in middleware-codegen.ts to keep runtime and generated code
in sync.
* fix: support constraints on catch-all params and add negative optional test
Address review feedback:
- Add :param*(constraint) and :param+(constraint) support so catch-all
params can also have inline regex constraints (e.g. /:path*(api|static)).
Update hasConstraints detection to match :param[*+]?( patterns.
- Add negative test for optional constrained params: /de/about must not
match /:locale(en|es|fr)?/about since de is not in the alternation.
* fix: emit hashChangeStart/Complete and beforeHistoryChange events
Next.js emits hashChangeStart/hashChangeComplete for hash-only
navigation and beforeHistoryChange before every history.pushState/
replaceState call. Our router emitted neither.
- Hash-only push/replace now emit hashChangeStart before and
hashChangeComplete after (instead of routeChange* events)
- Normal push/replace now emit beforeHistoryChange between
routeChangeStart and the pushState/replaceState call
- All events now pass { shallow } as the second argument per Next.js
- Popstate handler also emits beforeHistoryChange
- Updated in both useRouter() hook and Router singleton code paths
* fix: emit full URL in hash change events, not just fragment
When router.push("#section") is called, emit the full resolved URL
(e.g., "/page#section") in hashChangeStart/hashChangeComplete events
instead of just "#section". Matches Next.js behavior where the event
URL includes the pathname.
* fix: handle hash-only popstate and address review feedback
- Detect hash-only back/forward in popstate handler by tracking
pathname+search across navigations; emit hashChangeStart/Complete
instead of routeChangeStart/Complete and skip unnecessary page fetch
- Add comment explaining beforeHistoryChange semantics in popstate
(fires after URL change, emitted for API compatibility)
- Cache resolveHashUrl() result in all 4 hash code paths
- Add beforeHistoryChange negative assertions in hash-only tests
- Add E2E test for hash-only back/forward navigation events
* fix: pass { shallow } to routeChangeError, add goForward hash test
- routeChangeError now passes { shallow: false } to match Next.js
signature (err, url, { shallow }), completing the { shallow }
migration across all router events
- Add E2E test for hash-only goForward() popstate detection
* fix: include hash in popstate beforeHistoryChange URL, stabilize forward test
- beforeHistoryChange in popstate now emits appUrl + window.location.hash
for consistency with push/replace paths that preserve the hash fragment
- Add waitForFunction between goBack() and clear-events in the forward
test to prevent race between popstate handler and event clearing
* fix: strip basePath from full-path hash URLs, include hash in popstate event URLs
- resolveHashUrl now handles full-path hash URLs (e.g. /base/page#hash)
by parsing and stripping basePath, not just fragment-only URLs (#foo)
- routeChangeStart and routeChangeComplete in popstate now include
window.location.hash for consistency with beforeHistoryChange and
the push/replace code paths
* fix: update _lastPathnameAndSearch after beforePopState check
Move tracker update to after beforePopState confirms navigation
proceeds. Previously, if beforePopState returned false (cancelling
navigation), the tracker was already updated to the new browserUrl,
causing the next popstate to compare against the wrong baseline and
potentially misdetect a route change as hash-only.
* fix: make useParams() reactive on client-side navigation
useParams() was reading a module-level variable directly instead of
subscribing via useSyncExternalStore, so navigating between dynamic
routes (e.g. /posts/1 → /posts/2) wouldn't re-render components.
Two changes:
- useParams() now uses useSyncExternalStore with the shared listener
system, matching usePathname() and useSearchParams()
- setClientParams() now calls notifyListeners() when params change,
so the external store knows to re-check snapshots
* chore: fix formatting in test fixture
* fix: stabilize useParams server snapshot and deduplicate subscribe
- Add _EMPTY_PARAMS constant to avoid creating a new {} on every
getServerSnapshot call (Object.is would never match, causing
hydration churn)
- Extract getClientParamsSnapshot/getServerParamsSnapshot named
functions for consistency with usePathname/useSearchParams
- Extract subscribeToNavigation helper to deduplicate the identical
subscribe callback across all three hooks
On Cloudflare Workers the isolate can terminate after the Response is
sent, killing in-flight error reports (e.g. Sentry HTTP requests).
Fix by having reportRequestError() self-register with
ctx.waitUntil() via getRequestExecutionContext(). This is the same
pattern used by KVCacheHandler, ISR regeneration, and fetch cache
revalidation. On Node.js (dev or vinext start) the context is null
so it's a no-op — fire-and-forget is fine because the process stays
alive.
Since reportRequestError never rejects (internal try/catch), the
redundant .catch() at all 5 call sites is removed.
* fix: Pages Router ISR background regeneration re-renders HTML
Background regeneration only re-ran getStaticProps but cached the old
HTML, so server-rendered content was permanently stale after the first
revalidation cycle. The pageData JSON was updated but never used to
re-render the page.
Now the regeneration callback:
- Re-renders the page component with fresh props
- Rebuilds __NEXT_DATA__ with fresh props
- Updates the revalidate duration map (dev server)
- Passes full locale context to getStaticProps
Fixed in both dev server and production server entry.
* ci: retrigger checks
* fix: preserve document shell during prod ISR regeneration
The prod entry's regeneration was hardcoding a simple HTML suffix,
losing custom _document content between <Main /> and <NextScript />.
Now splits the cached HTML at known markers (<div id="__next"> and
__NEXT_DATA__ script) to preserve the full document structure: head
section, gap content, and tail are carried forward from the original
cached entry.
Also updates the entry-templates snapshot.
* ci: retrigger checks
* ci: retrigger checks
The dev server read middleware custom response bodies with `text()`,
which decodes bytes as UTF-8. This corrupts any binary response body
(images, protobuf, etc.) that middleware returns via `new Response(data)`.
The prod server already uses `arrayBuffer()` + `Buffer.from()` correctly.
Apply the same approach to the dev path.
Also adds middleware fixture routes for binary responses and multi-cookie
responses, with corresponding regression tests.
If SSR begins streaming then throws, res.writeHead(500) is called on an
already-sent response, causing a silent socket error. The App Router path
already has this guard — add the same check to the Pages Router path.
* fix: exclude private folders (_prefix) from App Router route discovery
Next.js treats underscore-prefixed directories in app/ as "private
folders" that are excluded from routing. This is a recommended pattern
for colocating non-route files (e.g. _components, _lib, _utils).
vinext was only excluding @-prefixed slot directories, so files like
app/_components/page.tsx would incorrectly create routes.
Add the underscore check to all four App Router filesystem scan points:
- Main page/route discovery (scanWithExtensions exclude callback)
- Slot sub-page scanner (already had it)
- Intercepting route scanner (scanForInterceptingPages)
- Intercepting page collector (collectInterceptingPages)
* chore: format app-router.ts
* fix: add headersSent guard in API route error handler
If a Pages Router API handler partially writes a response (e.g.
res.writeHead(200)) and then throws, the catch block would crash with
ERR_HTTP_HEADERS_SENT when trying to write a 500/413 response.
This matches the existing guard pattern in prod-server.ts and index.ts.
* fix: end response when headers already sent but stream not closed
If a handler calls res.writeHead()/res.write() then throws, headersSent
is true but writableEnded is false. Without res.end() the connection
hangs until timeout. Now we close the response in that case.
* chore: retrigger CI
* fix: call reportRequestError in Pages Router prod entry
Pages Router production (pages-server-entry.ts) was missing
reportRequestError() calls in the SSR and API catch blocks.
Dev (dev-server.ts, api-handler.ts) and App Router prod
(app-rsc-entry.ts) both reported errors, so instrumentation
hooks like Sentry's onRequestError silently stopped firing
in Pages Router production only.
* fix: use original URL path in SSR error report, not locale-stripped
routeUrl has the locale prefix stripped by extractLocale(), so i18n
requests like /fr/about would be reported as /about. Use url (the
original request path) to match dev-server.ts behavior.