From f64ea9fa457ccc7bc04f294ec1045445c166aaf2 Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Sat, 4 Apr 2026 11:17:28 +0530 Subject: [PATCH 1/2] fix(rewrites): include middleware headers in static file responses - Add CONTENT_TYPES allowlist for MIME type detection - Apply middleware-set headers (like Set-Cookie) to static file responses - Serve static files for beforeFiles and fallback rewrite targets --- packages/vinext/src/index.ts | 91 +++++++++++++++-------- packages/vinext/src/server/prod-server.ts | 14 ++++ 2 files changed, 76 insertions(+), 29 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 98ac869ea..a7ca26d1e 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -845,6 +845,29 @@ export interface VinextOptions { }; } +/** Content-type lookup for static assets. */ +const CONTENT_TYPES: Record = { + ".js": "application/javascript", + ".mjs": "application/javascript", + ".css": "text/css", + ".html": "text/html", + ".json": "application/json", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", + ".eot": "application/vnd.ms-fontobject", + ".webp": "image/webp", + ".avif": "image/avif", + ".map": "application/json", + ".rsc": "text/x-component", +}; + export default function vinext(options: VinextOptions = {}): PluginOption[] { const viteMajorVersion = getViteMajorVersion(); let root: string; @@ -2589,6 +2612,31 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // (app router is handled by @vitejs/plugin-rsc's built-in middleware) if (!hasPagesDir) return next(); + const applyRequestHeadersToNodeRequest = (nextRequestHeaders: Headers) => { + for (const key of Object.keys(req.headers)) { + delete req.headers[key]; + } + for (const [key, value] of nextRequestHeaders) { + req.headers[key] = value; + } + }; + + let middlewareRequestHeaders: Headers | null = null; + let deferredMwResponseHeaders: [string, string][] | null = null; + + const applyDeferredMwHeaders = ( + response: import("node:http").ServerResponse, + headers?: [string, string][] | Headers | null, + ) => { + if (!headers) return; + for (const [key, value] of headers) { + // skip internal x-middleware- headers + if (key.startsWith("x-middleware-")) continue; + // append handles multiple Set-Cookie correctly + response.appendHeader(key, value); + } + }; + // Skip Vite internal requests and static files if ( url.startsWith("/@") || @@ -2674,8 +2722,13 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } // Skip requests for files with extensions (static assets) - let pathname = url.split("?")[0]; - if (pathname.includes(".") && !pathname.endsWith(".html")) { + const [pathnameWithExt] = url.split("?"); + const ext = path.extname(pathnameWithExt); + if (ext && ext !== ".html" && CONTENT_TYPES[ext]) { + // If middleware was run, apply its headers (Set-Cookie, etc.) + // before Vite's built-in static-file middleware sends the file. + // This ensures public/ asset responses have middleware headers. + applyDeferredMwHeaders(res, deferredMwResponseHeaders); return next(); } @@ -2683,7 +2736,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Normalize backslashes first: browsers treat /\ as // in URL // context. Check the RAW pathname before normalizePath so the // guard fires before normalizePath collapses //. - pathname = pathname.replaceAll("\\", "/"); + let pathname = pathnameWithExt.replaceAll("\\", "/"); if (pathname.startsWith("//")) { res.writeHead(404); res.end("404 Not Found"); @@ -2783,26 +2836,6 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (redirected) return; } - const applyRequestHeadersToNodeRequest = (nextRequestHeaders: Headers) => { - for (const key of Object.keys(req.headers)) { - delete req.headers[key]; - } - for (const [key, value] of nextRequestHeaders) { - req.headers[key] = value; - } - }; - - let middlewareRequestHeaders: Headers | null = null; - let deferredMwResponseHeaders: [string, string][] | null = null; - - const applyDeferredMwHeaders = () => { - if (deferredMwResponseHeaders) { - for (const [key, value] of deferredMwResponseHeaders) { - res.appendHeader(key, value); - } - } - }; - // Run middleware.ts if present if (middlewarePath) { // Only trust X-Forwarded-Proto when behind a trusted proxy @@ -2968,7 +3001,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // External rewrite from beforeFiles — proxy to external URL if (isExternalUrl(resolvedUrl)) { - applyDeferredMwHeaders(); + applyDeferredMwHeaders(res, deferredMwResponseHeaders); await proxyExternalRewriteNode(req, res, resolvedUrl); return; } @@ -2983,7 +3016,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ); const apiMatch = matchRoute(resolvedUrl, apiRoutes); if (apiMatch) { - applyDeferredMwHeaders(); + applyDeferredMwHeaders(res, deferredMwResponseHeaders); if (middlewareRequestHeaders) { applyRequestHeadersToNodeRequest(middlewareRequestHeaders); } @@ -3023,7 +3056,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // External rewrite from afterFiles — proxy to external URL if (isExternalUrl(resolvedUrl)) { - applyDeferredMwHeaders(); + applyDeferredMwHeaders(res, deferredMwResponseHeaders); await proxyExternalRewriteNode(req, res, resolvedUrl); return; } @@ -3043,7 +3076,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Try rendering the resolved URL const match = matchRoute(resolvedUrl.split("?")[0], routes); if (match) { - applyDeferredMwHeaders(); + applyDeferredMwHeaders(res, deferredMwResponseHeaders); if (middlewareRequestHeaders) { applyRequestHeadersToNodeRequest(middlewareRequestHeaders); } @@ -3061,7 +3094,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (fallbackRewrite) { // External fallback rewrite — proxy to external URL if (isExternalUrl(fallbackRewrite)) { - applyDeferredMwHeaders(); + applyDeferredMwHeaders(res, deferredMwResponseHeaders); await proxyExternalRewriteNode(req, res, fallbackRewrite); return; } @@ -3069,7 +3102,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (!fallbackMatch && hasAppDir) { return next(); } - applyDeferredMwHeaders(); + applyDeferredMwHeaders(res, deferredMwResponseHeaders); if (middlewareRequestHeaders) { applyRequestHeadersToNodeRequest(middlewareRequestHeaders); } diff --git a/packages/vinext/src/server/prod-server.ts b/packages/vinext/src/server/prod-server.ts index a09562c77..145725fc6 100644 --- a/packages/vinext/src/server/prod-server.ts +++ b/packages/vinext/src/server/prod-server.ts @@ -1238,6 +1238,13 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { } resolvedUrl = rewritten; resolvedPathname = rewritten.split("?")[0]; + + if ( + path.extname(resolvedPathname) && + tryServeStatic(req, res, clientDir, resolvedPathname, compress, middlewareHeaders) + ) { + return; + } } } @@ -1259,6 +1266,13 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { await sendWebResponse(proxyResponse, req, res, compress); return; } + const fallbackPathname = fallbackRewrite.split("?")[0]; + if ( + path.extname(fallbackPathname) && + tryServeStatic(req, res, clientDir, fallbackPathname, compress, middlewareHeaders) + ) { + return; + } response = await renderPage(webRequest, fallbackRewrite, ssrManifest); } } From b30ad10ca7ef924768ac439236172b81b785fa40 Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Sun, 5 Apr 2026 16:42:12 +0530 Subject: [PATCH 2/2] fix(prod-server): serve static files for beforeFiles rewrite targets Add static file serving for beforeFiles rewrite targets in the Pages Router production server. This matches Next.js behavior where beforeFiles rewrites can resolve to static files in public/ or other filesystem paths. The fix passes middleware headers (including Set-Cookie) to the static file response, ensuring middleware-set headers appear on static assets. This mirrors the existing behavior for afterFiles and fallback rewrites. --- packages/vinext/src/server/prod-server.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/vinext/src/server/prod-server.ts b/packages/vinext/src/server/prod-server.ts index 145725fc6..1f3ec28d4 100644 --- a/packages/vinext/src/server/prod-server.ts +++ b/packages/vinext/src/server/prod-server.ts @@ -1181,6 +1181,9 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { } // ── 7. Apply beforeFiles rewrites from next.config.js ───────── + // Serve static files for beforeFiles rewrite targets. This matches + // Next.js behavior where beforeFiles rewrites can resolve to static + // files in public/ or other direct filesystem paths. if (configRewrites.beforeFiles?.length) { const rewritten = matchRewrite(resolvedPathname, configRewrites.beforeFiles, postMwReqCtx); if (rewritten) { @@ -1191,6 +1194,14 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { } resolvedUrl = rewritten; resolvedPathname = rewritten.split("?")[0]; + + // Try serving static file at the rewritten path + if ( + path.extname(resolvedPathname) && + tryServeStatic(req, res, clientDir, resolvedPathname, compress, middlewareHeaders) + ) { + return; + } } }