Documentation Index Fetch the complete documentation index at: https://mintlify.com/cloudflare/vinext/llms.txt
Use this file to discover all available pages before exploring further.
React Server Components Integration
Vinext’s App Router implementation is built on top of @vitejs/plugin-rsc, which provides the bundler transforms and runtime infrastructure for React Server Components. This guide explains how the integration works and the patterns used.
RSC Entry Point
The RSC entry (virtual:vinext-rsc-entry) is the request handler for all App Router requests. It runs in the rsc Vite environment with the react-server import condition.
Request Handler Export
From packages/vinext/src/server/app-dev-server.ts:
export default async function handler ( request ) {
// Wrap in AsyncLocalStorage context
const headersCtx = headersContextFromRequest ( request );
return runWithHeadersContext ( headersCtx , async () => {
_initRequestScopedCacheState ();
_clearPrivateCache ();
return runWithFetchCache ( async () => {
const response = await _handleRequest ( request );
// Apply custom headers from next.config.js
if ( __configHeaders . length && response ?. headers ) {
const extraHeaders = __applyConfigHeaders ( pathname );
for ( const h of extraHeaders ) {
response . headers . set ( h . key , h . value );
}
}
return response ;
});
});
}
Key patterns:
All async context uses AsyncLocalStorage.run() for proper isolation
Headers and cookies are available via headers() and cookies() throughout the tree
Per-request cache state is initialized once
Fetch cache tracks tags for revalidation
Request Lifecycle
The _handleRequest function implements the full request lifecycle:
Protocol-relative URL guard - Reject paths starting with //
Base path stripping - Remove basePath prefix if configured
Trailing slash normalization - Redirect to canonical form
Config redirects - Apply redirects from next.config.js
beforeFiles rewrites - Apply before file-system routing
Middleware execution - Run middleware.ts if path matches
Image optimization - Handle /_vinext/image endpoint
Metadata routes - Serve sitemap.xml, robots.txt, manifest.json
Server actions - Handle POST requests with x-rsc-action header
afterFiles rewrites - Apply after file-system routing
Route matching - Find matching App Router route
fallback rewrites - Apply if no route matched
Route handler execution - Run route.ts if present
Page rendering - Build component tree and render to RSC stream
SSR delegation - Pass RSC stream to SSR entry for HTML generation
Component Tree Rendering
Metadata and viewport are resolved from layouts and pages before rendering:
const metadataList = [];
const viewportList = [];
// Collect from layouts (root to leaf)
for ( const layoutMod of route . layouts ) {
if ( layoutMod ) {
const meta = await resolveModuleMetadata ( layoutMod , params );
if ( meta ) metadataList . push ( meta );
const vp = await resolveModuleViewport ( layoutMod , params );
if ( vp ) viewportList . push ( vp );
}
}
// Collect from page
if ( route . page ) {
const pageMeta = await resolveModuleMetadata ( route . page , params );
if ( pageMeta ) metadataList . push ( pageMeta );
const pageVp = await resolveModuleViewport ( route . page , params );
if ( pageVp ) viewportList . push ( pageVp );
}
const resolvedMetadata = metadataList . length > 0
? mergeMetadata ( metadataList )
: null ;
const resolvedViewport = viewportList . length > 0
? mergeViewport ( viewportList )
: null ;
resolveModuleMetadata() handles both static exports and generateMetadata() functions:
export async function resolveModuleMetadata (
module : any ,
params ?: Record < string , unknown >
) : Promise < Metadata | null > {
// Static export
if ( module . metadata && typeof module . metadata === "object" ) {
return module . metadata ;
}
// generateMetadata function
if ( typeof module . generateMetadata === "function" ) {
const asyncParams = Object . assign ( Promise . resolve ( params || {}), params || {});
return await module . generateMetadata ({ params: asyncParams });
}
return null ;
}
Thenable Params
Next.js 15+ changed params and searchParams to Promises. Vinext creates “thenable objects” for backward compatibility:
// Works both as Promise (new style) and object (old style)
const asyncParams = Object . assign ( Promise . resolve ( params ), params );
const pageProps = { params: asyncParams };
// New style (Next.js 15+):
const { id } = await params ;
// Old style (pre-15):
const { id } = params ;
This pattern is applied to:
Page component props
Layout component props
generateMetadata() arguments
generateViewport() arguments
Boundary Components
Vinext wraps the component tree with error, loading, and not-found boundaries:
Loading Boundary:
if ( route . loading ?. default ) {
element = createElement (
Suspense ,
{ fallback: createElement ( route . loading . default ) },
element ,
);
}
Error Boundary:
if ( route . error ?. default ) {
element = createElement ( ErrorBoundary , {
fallback: route . error . default ,
children: element ,
});
}
NotFound Boundary:
const NotFoundComponent = route . notFound ?. default ;
if ( NotFoundComponent ) {
element = createElement ( NotFoundBoundary , {
fallback: createElement ( NotFoundComponent ),
children: element ,
});
}
Boundaries are interleaved with layouts so errors propagate correctly:
for ( let i = route . layouts . length - 1 ; i >= 0 ; i -- ) {
// Per-layout error boundary BEFORE layout
if ( route . errors && route . errors [ i ]?. default ) {
element = createElement ( ErrorBoundary , {
fallback: route . errors [ i ]. default ,
children: element ,
});
}
const LayoutComponent = route . layouts [ i ]?. default ;
if ( LayoutComponent ) {
// Per-layout NotFoundBoundary
const LayoutNotFound = route . notFounds ?.[ i ]?. default ;
if ( LayoutNotFound ) {
element = createElement ( NotFoundBoundary , {
fallback: createElement ( LayoutNotFound ),
children: element ,
});
}
element = createElement ( LayoutComponent , { children: element , params });
}
}
This ensures errors from Layout N are caught by the boundary at Layout N-1.
Server Actions
Server actions are POST requests with the x-rsc-action header:
CSRF Protection
Vinext implements the same CSRF protection as Next.js:
function __validateCsrfOrigin ( request ) {
const originHeader = request . headers . get ( "origin" );
if ( ! originHeader || originHeader === "null" ) return null ;
let originHost ;
try {
originHost = new URL ( originHeader ). host . toLowerCase ();
} catch {
return new Response ( "Forbidden" , { status: 403 });
}
const hostHeader = (
request . headers . get ( "x-forwarded-host" ) ||
request . headers . get ( "host" ) ||
""
). split ( "," )[ 0 ]. trim (). toLowerCase ();
// Same origin - allow
if ( originHost === hostHeader ) return null ;
// Check allowedOrigins from next.config.js
if ( __allowedOrigins . length > 0 && __isOriginAllowed ( originHost , __allowedOrigins )) {
return null ;
}
return new Response ( "Forbidden" , { status: 403 });
}
Action Execution
if ( request . method === "POST" && actionId ) {
const csrfResponse = __validateCsrfOrigin ( request );
if ( csrfResponse ) return csrfResponse ;
const contentType = request . headers . get ( "content-type" ) || "" ;
const body = contentType . startsWith ( "multipart/form-data" )
? await request . formData ()
: await request . text ();
const temporaryReferences = createTemporaryReferenceSet ();
const args = await decodeReply ( body , { temporaryReferences });
const action = await loadServerAction ( actionId );
let returnValue ;
try {
const data = await action . apply ( null , args );
returnValue = { ok: true , data };
} catch ( e ) {
// Detect redirect() thrown inside action
if ( e ?. digest ?. startsWith ( "NEXT_REDIRECT;" )) {
const parts = e . digest . split ( ";" );
actionRedirect = {
url: parts [ 2 ],
type: parts [ 1 ] || "replace" ,
status: parts [ 3 ] ? parseInt ( parts [ 3 ], 10 ) : 307 ,
};
returnValue = { ok: true , data: undefined };
} else {
returnValue = { ok: false , data: e };
}
}
// Re-render page after action
const element = buildPageElement ( route , params , undefined , url . searchParams );
const rscStream = renderToReadableStream (
{ root: element , returnValue },
{ temporaryReferences , onError: rscOnError },
);
return new Response ( rscStream , {
headers: { "Content-Type" : "text/x-component; charset=utf-8" }
});
}
Key details:
Actions support both FormData and text bodies
Temporary references enable streaming large payloads
redirect() in actions is detected via digest
Page is re-rendered after mutation to reflect changes
Cookies set during action are attached to response
SSR Delegation
After rendering the RSC stream, the RSC entry delegates to the SSR entry for HTML generation:
const rscStream = renderToReadableStream ( element , { onError: rscOnError });
// Collect font data from RSC environment
const fontData = {
links: _getSSRFontLinks (),
styles: _getSSRFontStyles (),
preloads: _getSSRFontPreloads (),
};
// Load SSR entry from separate environment
const ssrEntry = await import . meta . viteRsc . loadModule ( "ssr" , "index" );
// Pass RSC stream, navigation context, and font data
const htmlStream = await ssrEntry . handleSsr (
rscStream ,
_getNavigationContext (),
fontData
);
setHeadersContext ( null );
setNavigationContext ( null );
const respHeaders = { "Content-Type" : "text/html; charset=utf-8" };
const linkParts = ( fontData . preloads || []). map (
p => `< ${ p . href } >; rel=preload; as=font; type= ${ p . type } ; crossorigin`
);
if ( linkParts . length > 0 ) respHeaders [ "Link" ] = linkParts . join ( ", " );
return new Response ( htmlStream , { headers: respHeaders });
Why pass navigation context explicitly?
The RSC and SSR environments have separate module instances. Setting setNavigationContext() in the RSC environment doesn’t affect the SSR environment.
Client components rendered during SSR need pathname/searchParams/params. The SSR entry receives the context and calls its own setNavigationContext() before rendering.
Route Handlers
Route handlers (route.ts) are special-cased:
if ( route . routeHandler ) {
const handler = route . routeHandler ;
const method = request . method . toUpperCase ();
// Collect exported HTTP methods
const HTTP_METHODS = [ "GET" , "HEAD" , "POST" , "PUT" , "DELETE" , "PATCH" , "OPTIONS" ];
const exportedMethods = HTTP_METHODS . filter ( m => typeof handler [ m ] === "function" );
if ( exportedMethods . includes ( "GET" ) && ! exportedMethods . includes ( "HEAD" )) {
exportedMethods . push ( "HEAD" );
}
// Auto-implement OPTIONS
if ( method === "OPTIONS" && typeof handler [ "OPTIONS" ] !== "function" ) {
return new Response ( null , {
status: 204 ,
headers: { "Allow" : exportedMethods . join ( ", " ) },
});
}
// Auto-implement HEAD (run GET and strip body)
let handlerFn = handler [ method ] || handler [ "default" ];
let isAutoHead = false ;
if ( method === "HEAD" && typeof handler [ "HEAD" ] !== "function" && typeof handler [ "GET" ] === "function" ) {
handlerFn = handler [ "GET" ];
isAutoHead = true ;
}
if ( typeof handlerFn === "function" ) {
const response = await handlerFn ( request , { params });
// Attach pending cookies
const pendingCookies = getAndClearPendingCookies ();
if ( pendingCookies . length > 0 ) {
const newHeaders = new Headers ( response . headers );
for ( const cookie of pendingCookies ) {
newHeaders . append ( "Set-Cookie" , cookie );
}
return new Response (
isAutoHead ? null : response . body ,
{ status: response . status , headers: newHeaders }
);
}
return isAutoHead
? new Response ( null , { status: response . status , headers: response . headers })
: response ;
}
}
Error Handling
RSC onError Callback
Vinext provides an onError callback to preserve digests for navigation errors:
function rscOnError ( error ) {
if ( error && typeof error === "object" && "digest" in error ) {
return String ( error . digest );
}
return undefined ;
}
const rscStream = renderToReadableStream ( element , { onError: rscOnError });
Without this, React’s default onError returns undefined and the digest is lost. Client-side error boundaries can’t identify the error type (redirect, notFound, etc.).
Error Page Rendering
When a server component throws, Vinext renders the error boundary page:
async function renderErrorBoundaryPage ( route , error , isRscRequest , request ) {
// Resolve error boundary component (leaf → per-layout → global-error)
let ErrorComponent = route ?. error ?. default ?? null ;
if ( ! ErrorComponent && route ?. errors ) {
for ( let i = route . errors . length - 1 ; i >= 0 ; i -- ) {
if ( route . errors [ i ]?. default ) {
ErrorComponent = route . errors [ i ]. default ;
break ;
}
}
}
ErrorComponent = ErrorComponent ?? globalErrorModule ?. default ;
if ( ! ErrorComponent ) return null ;
const errorObj = error instanceof Error ? error : new Error ( String ( error ));
let element = createElement ( ErrorComponent , { error: errorObj });
// Wrap with layouts (for RSC requests, also wrap with LayoutSegmentProvider)
const layouts = route ?. layouts ?? rootLayouts ;
for ( let i = layouts . length - 1 ; i >= 0 ; i -- ) {
const LayoutComponent = layouts [ i ]?. default ;
if ( LayoutComponent ) {
element = createElement ( LayoutComponent , { children: element });
if ( isRscRequest ) {
const layoutDepth = route ?. layoutSegmentDepths ?.[ i ] ?? 0 ;
element = createElement ( LayoutSegmentProvider , { depth: layoutDepth }, element );
}
}
}
const rscStream = renderToReadableStream ( element , { onError: rscOnError });
// ... (delegate to SSR or return RSC stream)
}
Important: Next.js returns HTTP 200 when error.tsx catches an error (the error is “handled” by the boundary). Vinext matches this behavior.
Next Steps
Architecture Deep Dive Core architecture and design patterns
Build Pipeline Production build pipeline details
Virtual Modules Virtual module system and generation