@@ -1180,6 +1180,29 @@ export interface VinextOptions {
11801180 } ;
11811181}
11821182
1183+ /** Content-type lookup for static assets. */
1184+ const CONTENT_TYPES : Record < string , string > = {
1185+ ".js" : "application/javascript" ,
1186+ ".mjs" : "application/javascript" ,
1187+ ".css" : "text/css" ,
1188+ ".html" : "text/html" ,
1189+ ".json" : "application/json" ,
1190+ ".png" : "image/png" ,
1191+ ".jpg" : "image/jpeg" ,
1192+ ".jpeg" : "image/jpeg" ,
1193+ ".gif" : "image/gif" ,
1194+ ".svg" : "image/svg+xml" ,
1195+ ".ico" : "image/x-icon" ,
1196+ ".woff" : "font/woff" ,
1197+ ".woff2" : "font/woff2" ,
1198+ ".ttf" : "font/ttf" ,
1199+ ".eot" : "application/vnd.ms-fontobject" ,
1200+ ".webp" : "image/webp" ,
1201+ ".avif" : "image/avif" ,
1202+ ".map" : "application/json" ,
1203+ ".rsc" : "text/x-component" ,
1204+ } ;
1205+
11831206export default function vinext ( options : VinextOptions = { } ) : PluginOption [ ] {
11841207 const viteMajorVersion = getViteMajorVersion ( ) ;
11851208 let root : string ;
@@ -2957,6 +2980,31 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] {
29572980 // (app router is handled by @vitejs/plugin-rsc's built-in middleware)
29582981 if ( ! hasPagesDir ) return next ( ) ;
29592982
2983+ const applyRequestHeadersToNodeRequest = ( nextRequestHeaders : Headers ) => {
2984+ for ( const key of Object . keys ( req . headers ) ) {
2985+ delete req . headers [ key ] ;
2986+ }
2987+ for ( const [ key , value ] of nextRequestHeaders ) {
2988+ req . headers [ key ] = value ;
2989+ }
2990+ } ;
2991+
2992+ let middlewareRequestHeaders : Headers | null = null ;
2993+ let deferredMwResponseHeaders : [ string , string ] [ ] | null = null ;
2994+
2995+ const applyDeferredMwHeaders = (
2996+ response : import ( "node:http" ) . ServerResponse ,
2997+ headers ?: [ string , string ] [ ] | Headers | null ,
2998+ ) => {
2999+ if ( ! headers ) return ;
3000+ for ( const [ key , value ] of headers ) {
3001+ // skip internal x-middleware- headers
3002+ if ( key . startsWith ( "x-middleware-" ) ) continue ;
3003+ // append handles multiple Set-Cookie correctly
3004+ response . appendHeader ( key , value ) ;
3005+ }
3006+ } ;
3007+
29603008 // Skip Vite internal requests and static files
29613009 if (
29623010 url . startsWith ( "/@" ) ||
@@ -3042,16 +3090,21 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] {
30423090 }
30433091
30443092 // Skip requests for files with extensions (static assets)
3045- let pathname = url . split ( "?" ) [ 0 ] ;
3046- if ( pathname . includes ( "." ) && ! pathname . endsWith ( ".html" ) ) {
3093+ const [ pathnameWithExt ] = url . split ( "?" ) ;
3094+ const ext = path . extname ( pathnameWithExt ) ;
3095+ if ( ext && ext !== ".html" && CONTENT_TYPES [ ext ] ) {
3096+ // If middleware was run, apply its headers (Set-Cookie, etc.)
3097+ // before Vite's built-in static-file middleware sends the file.
3098+ // This ensures public/ asset responses have middleware headers.
3099+ applyDeferredMwHeaders ( res , deferredMwResponseHeaders ) ;
30473100 return next ( ) ;
30483101 }
30493102
30503103 // Guard against protocol-relative URL open redirects.
30513104 // Normalize backslashes first: browsers treat /\ as // in URL
30523105 // context. Check the RAW pathname before normalizePath so the
30533106 // guard fires before normalizePath collapses //.
3054- pathname = pathname . replaceAll ( "\\" , "/" ) ;
3107+ let pathname = pathnameWithExt . replaceAll ( "\\" , "/" ) ;
30553108 if ( pathname . startsWith ( "//" ) ) {
30563109 res . writeHead ( 404 ) ;
30573110 res . end ( "404 Not Found" ) ;
@@ -3151,26 +3204,6 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] {
31513204 if ( redirected ) return ;
31523205 }
31533206
3154- const applyRequestHeadersToNodeRequest = ( nextRequestHeaders : Headers ) => {
3155- for ( const key of Object . keys ( req . headers ) ) {
3156- delete req . headers [ key ] ;
3157- }
3158- for ( const [ key , value ] of nextRequestHeaders ) {
3159- req . headers [ key ] = value ;
3160- }
3161- } ;
3162-
3163- let middlewareRequestHeaders : Headers | null = null ;
3164- let deferredMwResponseHeaders : [ string , string ] [ ] | null = null ;
3165-
3166- const applyDeferredMwHeaders = ( ) => {
3167- if ( deferredMwResponseHeaders ) {
3168- for ( const [ key , value ] of deferredMwResponseHeaders ) {
3169- res . appendHeader ( key , value ) ;
3170- }
3171- }
3172- } ;
3173-
31743207 // Run middleware.ts if present
31753208 if ( middlewarePath ) {
31763209 // Only trust X-Forwarded-Proto when behind a trusted proxy
@@ -3336,7 +3369,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] {
33363369
33373370 // External rewrite from beforeFiles — proxy to external URL
33383371 if ( isExternalUrl ( resolvedUrl ) ) {
3339- applyDeferredMwHeaders ( ) ;
3372+ applyDeferredMwHeaders ( res , deferredMwResponseHeaders ) ;
33403373 await proxyExternalRewriteNode ( req , res , resolvedUrl ) ;
33413374 return ;
33423375 }
@@ -3351,7 +3384,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] {
33513384 ) ;
33523385 const apiMatch = matchRoute ( resolvedUrl , apiRoutes ) ;
33533386 if ( apiMatch ) {
3354- applyDeferredMwHeaders ( ) ;
3387+ applyDeferredMwHeaders ( res , deferredMwResponseHeaders ) ;
33553388 if ( middlewareRequestHeaders ) {
33563389 applyRequestHeadersToNodeRequest ( middlewareRequestHeaders ) ;
33573390 }
@@ -3391,7 +3424,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] {
33913424
33923425 // External rewrite from afterFiles — proxy to external URL
33933426 if ( isExternalUrl ( resolvedUrl ) ) {
3394- applyDeferredMwHeaders ( ) ;
3427+ applyDeferredMwHeaders ( res , deferredMwResponseHeaders ) ;
33953428 await proxyExternalRewriteNode ( req , res , resolvedUrl ) ;
33963429 return ;
33973430 }
@@ -3411,7 +3444,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] {
34113444 // Try rendering the resolved URL
34123445 const match = matchRoute ( resolvedUrl . split ( "?" ) [ 0 ] , routes ) ;
34133446 if ( match ) {
3414- applyDeferredMwHeaders ( ) ;
3447+ applyDeferredMwHeaders ( res , deferredMwResponseHeaders ) ;
34153448 if ( middlewareRequestHeaders ) {
34163449 applyRequestHeadersToNodeRequest ( middlewareRequestHeaders ) ;
34173450 }
@@ -3429,15 +3462,15 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] {
34293462 if ( fallbackRewrite ) {
34303463 // External fallback rewrite — proxy to external URL
34313464 if ( isExternalUrl ( fallbackRewrite ) ) {
3432- applyDeferredMwHeaders ( ) ;
3465+ applyDeferredMwHeaders ( res , deferredMwResponseHeaders ) ;
34333466 await proxyExternalRewriteNode ( req , res , fallbackRewrite ) ;
34343467 return ;
34353468 }
34363469 const fallbackMatch = matchRoute ( fallbackRewrite . split ( "?" ) [ 0 ] , routes ) ;
34373470 if ( ! fallbackMatch && hasAppDir ) {
34383471 return next ( ) ;
34393472 }
3440- applyDeferredMwHeaders ( ) ;
3473+ applyDeferredMwHeaders ( res , deferredMwResponseHeaders ) ;
34413474 if ( middlewareRequestHeaders ) {
34423475 applyRequestHeadersToNodeRequest ( middlewareRequestHeaders ) ;
34433476 }
0 commit comments