Skip to content

Commit 6df9ef5

Browse files
committed
fix(rewrites): include middleware headers in static file responses
When rewrites resolve to static files in public/, middleware response headers (Set-Cookie, security headers, etc.) were being silently dropped. This fix ensures middleware headers are merged into static file responses across all three server paths. Changes: - prod-server.ts: Pass middlewareHeaders to tryServeStatic() for both afterFiles and fallback rewrites - index.ts: Call applyDeferredMwHeaders() before sending static file responses; add CONTENT_TYPES map for MIME types; use try/catch for error handling This maintains parity with the existing tryServeStatic() call which already included middleware headers. Fixes #199
1 parent 0730add commit 6df9ef5

5 files changed

Lines changed: 86 additions & 41 deletions

File tree

packages/vinext/src/entries/app-rsc-entry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1956,7 +1956,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
19561956
headers: redirectHeaders,
19571957
});
19581958
1959-
// Append cookies (collected after rendering, not duplicated)
1959+
// Append cookies collected from action and redirect phases
19601960
if (actionPendingCookies.length > 0 || actionDraftCookie || redirectPendingCookies.length > 0 || redirectDraftCookie) {
19611961
for (const cookie of actionPendingCookies) {
19621962
redirectResponse.headers.append("Set-Cookie", cookie);

packages/vinext/src/index.ts

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
11831206
export 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
}

packages/vinext/src/server/app-browser-entry.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,17 +173,14 @@ function registerServerActionCallback(): void {
173173
window.history.replaceState(null, "", actionRedirect);
174174
}
175175

176-
// Notify subscribers (usePathname, useSearchParams, etc)
177-
notifyListeners();
178-
179176
// Read params from response header (same as normal RSC navigation)
180-
let params = {};
181177
const paramsHeader = fetchResponse.headers.get("X-Vinext-Params");
178+
let params: Record<string, string> = {};
182179
if (paramsHeader) {
183180
try {
184181
params = JSON.parse(decodeURIComponent(paramsHeader));
185182
} catch {
186-
// Ignore malformed params
183+
params = {};
187184
}
188185
}
189186

@@ -196,6 +193,7 @@ function registerServerActionCallback(): void {
196193
params,
197194
});
198195
setClientParams(params);
196+
notifyListeners();
199197

200198
// Handle return value if present
201199
if (result.returnValue) {

packages/vinext/src/server/prod-server.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,6 +1385,13 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) {
13851385
}
13861386
resolvedUrl = rewritten;
13871387
resolvedPathname = rewritten.split("?")[0];
1388+
1389+
if (
1390+
path.extname(resolvedPathname) &&
1391+
tryServeStatic(req, res, clientDir, resolvedPathname, compress, middlewareHeaders)
1392+
) {
1393+
return;
1394+
}
13881395
}
13891396
}
13901397

@@ -1406,6 +1413,13 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) {
14061413
await sendWebResponse(proxyResponse, req, res, compress);
14071414
return;
14081415
}
1416+
const fallbackPathname = fallbackRewrite.split("?")[0];
1417+
if (
1418+
path.extname(fallbackPathname) &&
1419+
tryServeStatic(req, res, clientDir, fallbackPathname, compress, middlewareHeaders)
1420+
) {
1421+
return;
1422+
}
14091423
response = await renderPage(webRequest, fallbackRewrite, ssrManifest);
14101424
}
14111425
}

tests/__snapshots__/entry-templates.test.ts.snap

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,7 +1674,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
16741674
headers: redirectHeaders,
16751675
});
16761676

1677-
// Append cookies (collected after rendering, not duplicated)
1677+
// Append cookies collected from action and redirect phases
16781678
if (actionPendingCookies.length > 0 || actionDraftCookie || redirectPendingCookies.length > 0 || redirectDraftCookie) {
16791679
for (const cookie of actionPendingCookies) {
16801680
redirectResponse.headers.append("Set-Cookie", cookie);
@@ -3974,7 +3974,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
39743974
headers: redirectHeaders,
39753975
});
39763976

3977-
// Append cookies (collected after rendering, not duplicated)
3977+
// Append cookies collected from action and redirect phases
39783978
if (actionPendingCookies.length > 0 || actionDraftCookie || redirectPendingCookies.length > 0 || redirectDraftCookie) {
39793979
for (const cookie of actionPendingCookies) {
39803980
redirectResponse.headers.append("Set-Cookie", cookie);
@@ -6280,7 +6280,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
62806280
headers: redirectHeaders,
62816281
});
62826282

6283-
// Append cookies (collected after rendering, not duplicated)
6283+
// Append cookies collected from action and redirect phases
62846284
if (actionPendingCookies.length > 0 || actionDraftCookie || redirectPendingCookies.length > 0 || redirectDraftCookie) {
62856285
for (const cookie of actionPendingCookies) {
62866286
redirectResponse.headers.append("Set-Cookie", cookie);
@@ -8610,7 +8610,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
86108610
headers: redirectHeaders,
86118611
});
86128612

8613-
// Append cookies (collected after rendering, not duplicated)
8613+
// Append cookies collected from action and redirect phases
86148614
if (actionPendingCookies.length > 0 || actionDraftCookie || redirectPendingCookies.length > 0 || redirectDraftCookie) {
86158615
for (const cookie of actionPendingCookies) {
86168616
redirectResponse.headers.append("Set-Cookie", cookie);
@@ -10914,7 +10914,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1091410914
headers: redirectHeaders,
1091510915
});
1091610916

10917-
// Append cookies (collected after rendering, not duplicated)
10917+
// Append cookies collected from action and redirect phases
1091810918
if (actionPendingCookies.length > 0 || actionDraftCookie || redirectPendingCookies.length > 0 || redirectDraftCookie) {
1091910919
for (const cookie of actionPendingCookies) {
1092010920
redirectResponse.headers.append("Set-Cookie", cookie);
@@ -13571,7 +13571,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1357113571
headers: redirectHeaders,
1357213572
});
1357313573

13574-
// Append cookies (collected after rendering, not duplicated)
13574+
// Append cookies collected from action and redirect phases
1357513575
if (actionPendingCookies.length > 0 || actionDraftCookie || redirectPendingCookies.length > 0 || redirectDraftCookie) {
1357613576
for (const cookie of actionPendingCookies) {
1357713577
redirectResponse.headers.append("Set-Cookie", cookie);

0 commit comments

Comments
 (0)