-
Notifications
You must be signed in to change notification settings - Fork 305
fix(rewrites): include middleware headers in static file responses #776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -845,6 +845,29 @@ export interface VinextOptions { | |||||
| }; | ||||||
| } | ||||||
|
|
||||||
| /** Content-type lookup for static assets. */ | ||||||
| const CONTENT_TYPES: Record<string, string> = { | ||||||
| ".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 = ( | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The refactored On |
||||||
| 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,16 +2722,21 @@ 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]) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Behavioral regression: the old guard was This means files with unlisted extensions ( If the goal is to apply middleware headers to static files, the extension allowlist approach is the wrong tool — keep the original broad check and apply headers conditionally:
Suggested change
|
||||||
| // 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); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This For middleware headers to actually appear on static file responses, either:
Both options change the execution order, so this needs careful thought about whether it matches Next.js semantics. Per the Next.js execution order, middleware runs before filesystem routes — so option (1) or (2) would actually be more correct for |
||||||
| return next(); | ||||||
| } | ||||||
|
|
||||||
| // Guard against protocol-relative URL open redirects. | ||||||
| // 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,15 +3094,15 @@ 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; | ||||||
| } | ||||||
| const fallbackMatch = matchRoute(fallbackRewrite.split("?")[0], routes); | ||||||
| if (!fallbackMatch && hasAppDir) { | ||||||
| return next(); | ||||||
| } | ||||||
| applyDeferredMwHeaders(); | ||||||
| applyDeferredMwHeaders(res, deferredMwResponseHeaders); | ||||||
| if (middlewareRequestHeaders) { | ||||||
| applyRequestHeadersToNodeRequest(middlewareRequestHeaders); | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1238,6 +1249,13 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { | |
| } | ||
| resolvedUrl = rewritten; | ||
| resolvedPathname = rewritten.split("?")[0]; | ||
|
|
||
| if ( | ||
| path.extname(resolvedPathname) && | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change won't apply to current After rebasing, this logic needs to be re-implemented using the new |
||
| tryServeStatic(req, res, clientDir, resolvedPathname, compress, middlewareHeaders) | ||
| ) { | ||
| return; | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1259,6 +1277,13 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { | |
| await sendWebResponse(proxyResponse, req, res, compress); | ||
| return; | ||
| } | ||
| const fallbackPathname = fallbackRewrite.split("?")[0]; | ||
| if ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue as the Also note: the |
||
| path.extname(fallbackPathname) && | ||
| tryServeStatic(req, res, clientDir, fallbackPathname, compress, middlewareHeaders) | ||
| ) { | ||
| return; | ||
| } | ||
| response = await renderPage(webRequest, fallbackRewrite, ssrManifest); | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
CONTENT_TYPESmap is a duplicate of the one inprod-server.ts(line 226). On currentmain, it has been further extracted intoserver/static-file-cache.ts. Adding a second copy creates a maintenance hazard — when one is updated the other is easily forgotten.After rebasing onto
main, importCONTENT_TYPESfromstatic-file-cache.tsinstead of duplicating it.