vinext 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -1
- package/dist/client/entry.js +1 -15
- package/dist/client/entry.js.map +1 -1
- package/dist/client/validate-module-path.d.ts +15 -0
- package/dist/client/validate-module-path.d.ts.map +1 -0
- package/dist/client/validate-module-path.js +31 -0
- package/dist/client/validate-module-path.js.map +1 -0
- package/dist/config/config-matchers.d.ts +12 -0
- package/dist/config/config-matchers.d.ts.map +1 -1
- package/dist/config/config-matchers.js +28 -0
- package/dist/config/config-matchers.js.map +1 -1
- package/dist/config/dotenv.d.ts +40 -0
- package/dist/config/dotenv.d.ts.map +1 -0
- package/dist/config/dotenv.js +100 -0
- package/dist/config/dotenv.js.map +1 -0
- package/dist/config/next-config.d.ts +4 -0
- package/dist/config/next-config.d.ts.map +1 -1
- package/dist/config/next-config.js.map +1 -1
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +16 -8
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -111
- package/dist/index.js.map +1 -1
- package/dist/server/api-handler.d.ts.map +1 -1
- package/dist/server/api-handler.js +2 -1
- package/dist/server/api-handler.js.map +1 -1
- package/dist/server/app-dev-server.d.ts +2 -0
- package/dist/server/app-dev-server.d.ts.map +1 -1
- package/dist/server/app-dev-server.js +292 -155
- package/dist/server/app-dev-server.js.map +1 -1
- package/dist/server/app-router-entry.d.ts.map +1 -1
- package/dist/server/app-router-entry.js +16 -3
- package/dist/server/app-router-entry.js.map +1 -1
- package/dist/server/dev-origin-check.d.ts +61 -0
- package/dist/server/dev-origin-check.d.ts.map +1 -0
- package/dist/server/dev-origin-check.js +164 -0
- package/dist/server/dev-origin-check.js.map +1 -0
- package/dist/server/dev-server.d.ts +0 -2
- package/dist/server/dev-server.d.ts.map +1 -1
- package/dist/server/dev-server.js +379 -372
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/image-optimization.d.ts +32 -2
- package/dist/server/image-optimization.d.ts.map +1 -1
- package/dist/server/image-optimization.js +110 -9
- package/dist/server/image-optimization.js.map +1 -1
- package/dist/server/middleware-codegen.d.ts +41 -0
- package/dist/server/middleware-codegen.d.ts.map +1 -0
- package/dist/server/middleware-codegen.js +181 -0
- package/dist/server/middleware-codegen.js.map +1 -0
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +12 -7
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/normalize-path.d.ts +22 -0
- package/dist/server/normalize-path.d.ts.map +1 -0
- package/dist/server/normalize-path.js +50 -0
- package/dist/server/normalize-path.js.map +1 -0
- package/dist/server/prod-server.d.ts.map +1 -1
- package/dist/server/prod-server.js +89 -25
- package/dist/server/prod-server.js.map +1 -1
- package/dist/shims/cache-runtime.d.ts +7 -0
- package/dist/shims/cache-runtime.d.ts.map +1 -1
- package/dist/shims/cache-runtime.js +19 -15
- package/dist/shims/cache-runtime.js.map +1 -1
- package/dist/shims/cache.d.ts +8 -0
- package/dist/shims/cache.d.ts.map +1 -1
- package/dist/shims/cache.js +20 -15
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/fetch-cache.d.ts +2 -3
- package/dist/shims/fetch-cache.d.ts.map +1 -1
- package/dist/shims/fetch-cache.js +74 -9
- package/dist/shims/fetch-cache.js.map +1 -1
- package/dist/shims/head-state.d.ts +6 -1
- package/dist/shims/head-state.d.ts.map +1 -1
- package/dist/shims/head-state.js +18 -15
- package/dist/shims/head-state.js.map +1 -1
- package/dist/shims/headers.d.ts +9 -13
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/headers.js +26 -47
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/image.d.ts.map +1 -1
- package/dist/shims/image.js +11 -2
- package/dist/shims/image.js.map +1 -1
- package/dist/shims/navigation-state.d.ts +6 -1
- package/dist/shims/navigation-state.d.ts.map +1 -1
- package/dist/shims/navigation-state.js +20 -29
- package/dist/shims/navigation-state.js.map +1 -1
- package/dist/shims/navigation.js +2 -2
- package/dist/shims/navigation.js.map +1 -1
- package/dist/shims/router-state.d.ts +6 -1
- package/dist/shims/router-state.d.ts.map +1 -1
- package/dist/shims/router-state.js +16 -21
- package/dist/shims/router-state.js.map +1 -1
- package/dist/shims/router.d.ts.map +1 -1
- package/dist/shims/router.js +19 -6
- package/dist/shims/router.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* the SSR entry for HTML generation.
|
|
8
8
|
*/
|
|
9
9
|
import fs from "node:fs";
|
|
10
|
+
import { generateDevOriginCheckCode } from "./dev-origin-check.js";
|
|
11
|
+
import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode } from "./middleware-codegen.js";
|
|
10
12
|
/**
|
|
11
13
|
* Generate the virtual RSC entry module.
|
|
12
14
|
*
|
|
@@ -199,11 +201,11 @@ import { LayoutSegmentProvider } from "vinext/layout-segment-context";
|
|
|
199
201
|
import { MetadataHead, mergeMetadata, resolveModuleMetadata, ViewportHead, mergeViewport, resolveModuleViewport } from "vinext/metadata";
|
|
200
202
|
${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
|
|
201
203
|
${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(new URL("./metadata-routes.js", import.meta.url).pathname.replace(/\\/g, "/"))};` : ""}
|
|
202
|
-
import { _consumeRequestScopedCacheLife,
|
|
204
|
+
import { _consumeRequestScopedCacheLife, _runWithCacheState } from "next/cache";
|
|
203
205
|
import { runWithFetchCache } from "vinext/fetch-cache";
|
|
204
|
-
import {
|
|
206
|
+
import { runWithPrivateCache as _runWithPrivateCache } from "vinext/cache-runtime";
|
|
205
207
|
// Import server-only state module to register ALS-backed accessors.
|
|
206
|
-
import "vinext/navigation-state";
|
|
208
|
+
import { runWithNavigationContext as _runWithNavigationContext } from "vinext/navigation-state";
|
|
207
209
|
import { reportRequestError as _reportRequestError } from "vinext/instrumentation";
|
|
208
210
|
import { getSSRFontLinks as _getSSRFontLinks, getSSRFontStyles as _getSSRFontStylesGoogle, getSSRFontPreloads as _getSSRFontPreloadsGoogle } from "next/font/google";
|
|
209
211
|
import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getSSRFontPreloadsLocal } from "next/font/local";
|
|
@@ -224,15 +226,64 @@ function setNavigationContext(ctx) {
|
|
|
224
226
|
// based on export const revalidate for testing purposes.
|
|
225
227
|
// Production ISR is handled by prod-server.ts and the Cloudflare worker entry.
|
|
226
228
|
|
|
229
|
+
// djb2 hash — matches Next.js's stringHash for digest generation.
|
|
230
|
+
// Produces a stable numeric string from error message + stack.
|
|
231
|
+
function __errorDigest(str) {
|
|
232
|
+
let hash = 5381;
|
|
233
|
+
for (let i = str.length - 1; i >= 0; i--) {
|
|
234
|
+
hash = (hash * 33) ^ str.charCodeAt(i);
|
|
235
|
+
}
|
|
236
|
+
return (hash >>> 0).toString();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sanitize an error for client consumption. In production, replaces the error
|
|
240
|
+
// with a generic Error that only carries a digest hash (matching Next.js
|
|
241
|
+
// behavior). In development, returns the original error for debugging.
|
|
242
|
+
// Navigation errors (redirect, notFound, etc.) are always passed through
|
|
243
|
+
// unchanged since their digests are used for client-side routing.
|
|
244
|
+
function __sanitizeErrorForClient(error) {
|
|
245
|
+
// Navigation errors must pass through with their digest intact
|
|
246
|
+
if (error && typeof error === "object" && "digest" in error) {
|
|
247
|
+
const digest = String(error.digest);
|
|
248
|
+
if (
|
|
249
|
+
digest.startsWith("NEXT_REDIRECT;") ||
|
|
250
|
+
digest === "NEXT_NOT_FOUND" ||
|
|
251
|
+
digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")
|
|
252
|
+
) {
|
|
253
|
+
return error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// In development, pass through the original error for debugging
|
|
257
|
+
if (process.env.NODE_ENV !== "production") {
|
|
258
|
+
return error;
|
|
259
|
+
}
|
|
260
|
+
// In production, create a sanitized error with only a digest hash
|
|
261
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
262
|
+
const stack = error instanceof Error ? (error.stack || "") : "";
|
|
263
|
+
const sanitized = new Error(
|
|
264
|
+
"An error occurred in the Server Components render. " +
|
|
265
|
+
"The specific message is omitted in production builds to avoid leaking sensitive details. " +
|
|
266
|
+
"A digest property is included on this error instance which may provide additional details about the nature of the error."
|
|
267
|
+
);
|
|
268
|
+
sanitized.digest = __errorDigest(msg + stack);
|
|
269
|
+
return sanitized;
|
|
270
|
+
}
|
|
271
|
+
|
|
227
272
|
// onError callback for renderToReadableStream — preserves the digest for
|
|
228
273
|
// Next.js navigation errors (redirect, notFound, forbidden, unauthorized)
|
|
229
274
|
// thrown during RSC streaming (e.g. inside Suspense boundaries).
|
|
230
|
-
//
|
|
231
|
-
//
|
|
275
|
+
// For non-navigation errors in production, generates a digest hash so the
|
|
276
|
+
// error can be correlated with server logs without leaking details.
|
|
232
277
|
function rscOnError(error) {
|
|
233
278
|
if (error && typeof error === "object" && "digest" in error) {
|
|
234
279
|
return String(error.digest);
|
|
235
280
|
}
|
|
281
|
+
// In production, generate a digest hash for non-navigation errors
|
|
282
|
+
if (process.env.NODE_ENV === "production" && error) {
|
|
283
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
284
|
+
const stack = error instanceof Error ? (error.stack || "") : "";
|
|
285
|
+
return __errorDigest(msg + stack);
|
|
286
|
+
}
|
|
236
287
|
return undefined;
|
|
237
288
|
}
|
|
238
289
|
|
|
@@ -333,7 +384,7 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
|
|
|
333
384
|
setNavigationContext(null);
|
|
334
385
|
return new Response(rscStream, {
|
|
335
386
|
status: statusCode,
|
|
336
|
-
headers: { "Content-Type": "text/x-component; charset=utf-8" },
|
|
387
|
+
headers: { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" },
|
|
337
388
|
});
|
|
338
389
|
}
|
|
339
390
|
// For HTML (full page load) responses, wrap with layouts only (no client-side
|
|
@@ -355,7 +406,7 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
|
|
|
355
406
|
const htmlStream = await ssrEntry.handleSsr(rscStream, _getNavigationContext(), fontData);
|
|
356
407
|
setHeadersContext(null);
|
|
357
408
|
setNavigationContext(null);
|
|
358
|
-
const _respHeaders = { "Content-Type": "text/html; charset=utf-8" };
|
|
409
|
+
const _respHeaders = { "Content-Type": "text/html; charset=utf-8", "Vary": "RSC, Accept" };
|
|
359
410
|
const _linkParts = (fontData.preloads || []).map(function(p) { return "<" + p.href + ">; rel=preload; as=font; type=" + p.type + "; crossorigin"; });
|
|
360
411
|
if (_linkParts.length > 0) _respHeaders["Link"] = _linkParts.join(", ");
|
|
361
412
|
return new Response(htmlStream, {
|
|
@@ -391,7 +442,11 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
|
|
|
391
442
|
ErrorComponent = ErrorComponent${globalErrorVar ? ` ?? ${globalErrorVar}?.default` : ""};
|
|
392
443
|
if (!ErrorComponent) return null;
|
|
393
444
|
|
|
394
|
-
const
|
|
445
|
+
const rawError = error instanceof Error ? error : new Error(String(error));
|
|
446
|
+
// Sanitize the error in production to avoid leaking internal details
|
|
447
|
+
// (database errors, file paths, stack traces) through error.tsx to the client.
|
|
448
|
+
// In development, pass the original error for debugging.
|
|
449
|
+
const errorObj = __sanitizeErrorForClient(rawError);
|
|
395
450
|
// Only pass error — reset is a client-side concern (re-renders the segment) and
|
|
396
451
|
// can't be serialized through RSC. The error.tsx component will receive reset=undefined
|
|
397
452
|
// during SSR, which is fine — onClick={undefined} is harmless, and the real reset
|
|
@@ -428,7 +483,7 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
|
|
|
428
483
|
setNavigationContext(null);
|
|
429
484
|
return new Response(rscStream, {
|
|
430
485
|
status: 200,
|
|
431
|
-
headers: { "Content-Type": "text/x-component; charset=utf-8" },
|
|
486
|
+
headers: { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" },
|
|
432
487
|
});
|
|
433
488
|
}
|
|
434
489
|
// For HTML (full page load) responses, wrap with layouts only.
|
|
@@ -449,7 +504,7 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
|
|
|
449
504
|
const htmlStream = await ssrEntry.handleSsr(rscStream, _getNavigationContext(), fontData);
|
|
450
505
|
setHeadersContext(null);
|
|
451
506
|
setNavigationContext(null);
|
|
452
|
-
const _errHeaders = { "Content-Type": "text/html; charset=utf-8" };
|
|
507
|
+
const _errHeaders = { "Content-Type": "text/html; charset=utf-8", "Vary": "RSC, Accept" };
|
|
453
508
|
const _errLinkParts = (fontData.preloads || []).map(function(p) { return "<" + p.href + ">; rel=preload; as=font; type=" + p.type + "; crossorigin"; });
|
|
454
509
|
if (_errLinkParts.length > 0) _errHeaders["Link"] = _errLinkParts.join(", ");
|
|
455
510
|
return new Response(htmlStream, {
|
|
@@ -765,23 +820,7 @@ async function buildPageElement(route, params, opts, searchParams) {
|
|
|
765
820
|
return element;
|
|
766
821
|
}
|
|
767
822
|
|
|
768
|
-
${middlewarePath ?
|
|
769
|
-
function matchMiddlewarePath(pathname, matcher) {
|
|
770
|
-
if (!matcher) return true;
|
|
771
|
-
const patterns = typeof matcher === "string" ? [matcher]
|
|
772
|
-
: Array.isArray(matcher) ? matcher.map(m => typeof m === "string" ? m : m.source)
|
|
773
|
-
: [];
|
|
774
|
-
return patterns.some(pattern => {
|
|
775
|
-
const reStr = "^" + pattern
|
|
776
|
-
.replace(/\\./g, "\\\\.")
|
|
777
|
-
.replace(/:(\\w+)\\*/g, "(?:.*)")
|
|
778
|
-
.replace(/:(\\w+)\\+/g, "(?:.+)")
|
|
779
|
-
.replace(/:(\\w+)/g, "([^/]+)") + "$";
|
|
780
|
-
const re = __safeRegExp(reStr);
|
|
781
|
-
return re ? re.test(pathname) : false;
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
` : ""}
|
|
823
|
+
${middlewarePath ? generateMiddlewareMatcherCode("modern") : ""}
|
|
785
824
|
|
|
786
825
|
const __basePath = ${JSON.stringify(bp)};
|
|
787
826
|
const __trailingSlash = ${JSON.stringify(ts)};
|
|
@@ -790,6 +829,8 @@ const __configRewrites = ${JSON.stringify(rewrites)};
|
|
|
790
829
|
const __configHeaders = ${JSON.stringify(headers)};
|
|
791
830
|
const __allowedOrigins = ${JSON.stringify(allowedOrigins)};
|
|
792
831
|
|
|
832
|
+
${generateDevOriginCheckCode(config?.allowedDevOrigins)}
|
|
833
|
+
|
|
793
834
|
// ── CSRF origin validation for server actions ───────────────────────────
|
|
794
835
|
// Matches Next.js behavior: compare the Origin header against the Host header.
|
|
795
836
|
// If they don't match, the request is rejected with 403 unless the origin is
|
|
@@ -822,8 +863,12 @@ function __validateCsrfOrigin(request) {
|
|
|
822
863
|
return new Response("Forbidden", { status: 403, headers: { "Content-Type": "text/plain" } });
|
|
823
864
|
}
|
|
824
865
|
|
|
866
|
+
// Only use the Host header for origin comparison — never trust
|
|
867
|
+
// X-Forwarded-Host here, since it can be freely set by the client
|
|
868
|
+
// and would allow the check to be bypassed if it matched a spoofed
|
|
869
|
+
// Origin. The prod server's resolveHost() handles trusted proxy
|
|
870
|
+
// scenarios separately.
|
|
825
871
|
const hostHeader = (
|
|
826
|
-
request.headers.get("x-forwarded-host") ||
|
|
827
872
|
request.headers.get("host") ||
|
|
828
873
|
""
|
|
829
874
|
).split(",")[0].trim().toLowerCase();
|
|
@@ -843,73 +888,10 @@ function __validateCsrfOrigin(request) {
|
|
|
843
888
|
}
|
|
844
889
|
|
|
845
890
|
// ── ReDoS-safe regex compilation ────────────────────────────────────────
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
while (i < pattern.length) {
|
|
851
|
-
const ch = pattern[i];
|
|
852
|
-
if (ch === "\\\\") { i += 2; continue; }
|
|
853
|
-
if (ch === "[") {
|
|
854
|
-
i++;
|
|
855
|
-
while (i < pattern.length && pattern[i] !== "]") {
|
|
856
|
-
if (pattern[i] === "\\\\") i++;
|
|
857
|
-
i++;
|
|
858
|
-
}
|
|
859
|
-
i++;
|
|
860
|
-
continue;
|
|
861
|
-
}
|
|
862
|
-
if (ch === "(") {
|
|
863
|
-
depth++;
|
|
864
|
-
if (quantifierAtDepth.length <= depth) quantifierAtDepth.push(false);
|
|
865
|
-
else quantifierAtDepth[depth] = false;
|
|
866
|
-
i++;
|
|
867
|
-
continue;
|
|
868
|
-
}
|
|
869
|
-
if (ch === ")") {
|
|
870
|
-
const hadQ = depth > 0 && quantifierAtDepth[depth];
|
|
871
|
-
if (depth > 0) depth--;
|
|
872
|
-
const next = pattern[i + 1];
|
|
873
|
-
if (next === "+" || next === "*" || next === "{") {
|
|
874
|
-
if (hadQ) return false;
|
|
875
|
-
if (depth >= 0 && depth < quantifierAtDepth.length) quantifierAtDepth[depth] = true;
|
|
876
|
-
}
|
|
877
|
-
i++;
|
|
878
|
-
continue;
|
|
879
|
-
}
|
|
880
|
-
if (ch === "+" || ch === "*") {
|
|
881
|
-
if (depth > 0) quantifierAtDepth[depth] = true;
|
|
882
|
-
i++;
|
|
883
|
-
continue;
|
|
884
|
-
}
|
|
885
|
-
if (ch === "?") {
|
|
886
|
-
const prev = i > 0 ? pattern[i - 1] : "";
|
|
887
|
-
if (prev !== "+" && prev !== "*" && prev !== "?" && prev !== "}") {
|
|
888
|
-
if (depth > 0) quantifierAtDepth[depth] = true;
|
|
889
|
-
}
|
|
890
|
-
i++;
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
if (ch === "{") {
|
|
894
|
-
let j = i + 1;
|
|
895
|
-
while (j < pattern.length && /[\\d,]/.test(pattern[j])) j++;
|
|
896
|
-
if (j < pattern.length && pattern[j] === "}" && j > i + 1) {
|
|
897
|
-
if (depth > 0) quantifierAtDepth[depth] = true;
|
|
898
|
-
i = j + 1;
|
|
899
|
-
continue;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
i++;
|
|
903
|
-
}
|
|
904
|
-
return true;
|
|
905
|
-
}
|
|
906
|
-
function __safeRegExp(pattern, flags) {
|
|
907
|
-
if (!__isSafeRegex(pattern)) {
|
|
908
|
-
console.warn("[vinext] Ignoring potentially unsafe regex pattern (ReDoS risk): " + pattern);
|
|
909
|
-
return null;
|
|
910
|
-
}
|
|
911
|
-
try { return new RegExp(pattern, flags); } catch { return null; }
|
|
912
|
-
}
|
|
891
|
+
${generateSafeRegExpCode("modern")}
|
|
892
|
+
|
|
893
|
+
// ── Path normalization ──────────────────────────────────────────────────
|
|
894
|
+
${generateNormalizePathCode("modern")}
|
|
913
895
|
|
|
914
896
|
// ── Config pattern matching (redirects, rewrites, headers) ──────────────
|
|
915
897
|
function __matchConfigPattern(pathname, pattern) {
|
|
@@ -1011,6 +993,12 @@ function __buildRequestContext(request) {
|
|
|
1011
993
|
};
|
|
1012
994
|
}
|
|
1013
995
|
|
|
996
|
+
function __sanitizeDestination(dest) {
|
|
997
|
+
if (dest.startsWith("http://") || dest.startsWith("https://")) return dest;
|
|
998
|
+
if (dest.startsWith("//")) dest = dest.replace(/^\\/\\/+/, "/");
|
|
999
|
+
return dest;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1014
1002
|
function __applyConfigRedirects(pathname, ctx) {
|
|
1015
1003
|
for (const rule of __configRedirects) {
|
|
1016
1004
|
const params = __matchConfigPattern(pathname, rule.source);
|
|
@@ -1018,6 +1006,7 @@ function __applyConfigRedirects(pathname, ctx) {
|
|
|
1018
1006
|
if (ctx && (rule.has || rule.missing)) { if (!__checkHasConditions(rule.has, rule.missing, ctx)) continue; }
|
|
1019
1007
|
let dest = rule.destination;
|
|
1020
1008
|
for (const [key, value] of Object.entries(params)) { dest = dest.replace(":" + key + "*", value); dest = dest.replace(":" + key + "+", value); dest = dest.replace(":" + key, value); }
|
|
1009
|
+
dest = __sanitizeDestination(dest);
|
|
1021
1010
|
return { destination: dest, permanent: rule.permanent };
|
|
1022
1011
|
}
|
|
1023
1012
|
}
|
|
@@ -1031,6 +1020,7 @@ function __applyConfigRewrites(pathname, rules, ctx) {
|
|
|
1031
1020
|
if (ctx && (rule.has || rule.missing)) { if (!__checkHasConditions(rule.has, rule.missing, ctx)) continue; }
|
|
1032
1021
|
let dest = rule.destination;
|
|
1033
1022
|
for (const [key, value] of Object.entries(params)) { dest = dest.replace(":" + key + "*", value); dest = dest.replace(":" + key + "+", value); dest = dest.replace(":" + key, value); }
|
|
1023
|
+
dest = __sanitizeDestination(dest);
|
|
1034
1024
|
return dest;
|
|
1035
1025
|
}
|
|
1036
1026
|
}
|
|
@@ -1041,6 +1031,71 @@ function __isExternalUrl(url) {
|
|
|
1041
1031
|
return url.startsWith("http://") || url.startsWith("https://");
|
|
1042
1032
|
}
|
|
1043
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* Maximum server-action request body size (1 MB).
|
|
1036
|
+
* Matches the Next.js default for serverActions.bodySizeLimit.
|
|
1037
|
+
* @see https://nextjs.org/docs/app/api-reference/config/next-config-js/serverActions#bodysizelimit
|
|
1038
|
+
* Prevents unbounded request body buffering.
|
|
1039
|
+
*/
|
|
1040
|
+
var __MAX_ACTION_BODY_SIZE = 1 * 1024 * 1024;
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Read a request body as text with a size limit.
|
|
1044
|
+
* Enforces the limit on the actual byte stream to prevent bypasses
|
|
1045
|
+
* via chunked transfer-encoding where Content-Length is absent or spoofed.
|
|
1046
|
+
*/
|
|
1047
|
+
async function __readBodyWithLimit(request, maxBytes) {
|
|
1048
|
+
if (!request.body) return "";
|
|
1049
|
+
var reader = request.body.getReader();
|
|
1050
|
+
var decoder = new TextDecoder();
|
|
1051
|
+
var chunks = [];
|
|
1052
|
+
var totalSize = 0;
|
|
1053
|
+
for (;;) {
|
|
1054
|
+
var result = await reader.read();
|
|
1055
|
+
if (result.done) break;
|
|
1056
|
+
totalSize += result.value.byteLength;
|
|
1057
|
+
if (totalSize > maxBytes) {
|
|
1058
|
+
reader.cancel();
|
|
1059
|
+
throw new Error("Request body too large");
|
|
1060
|
+
}
|
|
1061
|
+
chunks.push(decoder.decode(result.value, { stream: true }));
|
|
1062
|
+
}
|
|
1063
|
+
chunks.push(decoder.decode());
|
|
1064
|
+
return chunks.join("");
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Read a request body as FormData with a size limit.
|
|
1069
|
+
* Consumes the body stream with a byte counter and then parses the
|
|
1070
|
+
* collected bytes as multipart form data via the Response constructor.
|
|
1071
|
+
*/
|
|
1072
|
+
async function __readFormDataWithLimit(request, maxBytes) {
|
|
1073
|
+
if (!request.body) return new FormData();
|
|
1074
|
+
var reader = request.body.getReader();
|
|
1075
|
+
var chunks = [];
|
|
1076
|
+
var totalSize = 0;
|
|
1077
|
+
for (;;) {
|
|
1078
|
+
var result = await reader.read();
|
|
1079
|
+
if (result.done) break;
|
|
1080
|
+
totalSize += result.value.byteLength;
|
|
1081
|
+
if (totalSize > maxBytes) {
|
|
1082
|
+
reader.cancel();
|
|
1083
|
+
throw new Error("Request body too large");
|
|
1084
|
+
}
|
|
1085
|
+
chunks.push(result.value);
|
|
1086
|
+
}
|
|
1087
|
+
// Reconstruct a Response with the original Content-Type so that
|
|
1088
|
+
// the FormData parser can handle multipart boundaries correctly.
|
|
1089
|
+
var combined = new Uint8Array(totalSize);
|
|
1090
|
+
var offset = 0;
|
|
1091
|
+
for (var chunk of chunks) {
|
|
1092
|
+
combined.set(chunk, offset);
|
|
1093
|
+
offset += chunk.byteLength;
|
|
1094
|
+
}
|
|
1095
|
+
var contentType = request.headers.get("content-type") || "";
|
|
1096
|
+
return new Response(combined, { headers: { "Content-Type": contentType } }).formData();
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1044
1099
|
const __hopByHopHeaders = new Set(["connection","keep-alive","proxy-authenticate","proxy-authorization","te","trailers","transfer-encoding","upgrade"]);
|
|
1045
1100
|
|
|
1046
1101
|
async function __proxyExternalRequest(request, externalUrl) {
|
|
@@ -1086,49 +1141,62 @@ function __applyConfigHeaders(pathname) {
|
|
|
1086
1141
|
}
|
|
1087
1142
|
|
|
1088
1143
|
export default async function handler(request) {
|
|
1089
|
-
// Wrap the entire request
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1144
|
+
// Wrap the entire request in nested AsyncLocalStorage.run() scopes to ensure
|
|
1145
|
+
// per-request isolation for all state modules. Each runWith*() creates an
|
|
1146
|
+
// ALS scope that propagates through all async continuations (including RSC
|
|
1147
|
+
// streaming), preventing state leakage between concurrent requests on
|
|
1148
|
+
// Cloudflare Workers and other concurrent runtimes.
|
|
1092
1149
|
const headersCtx = headersContextFromRequest(request);
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1150
|
+
return runWithHeadersContext(headersCtx, () =>
|
|
1151
|
+
_runWithNavigationContext(() =>
|
|
1152
|
+
_runWithCacheState(() =>
|
|
1153
|
+
_runWithPrivateCache(() =>
|
|
1154
|
+
runWithFetchCache(async () => {
|
|
1155
|
+
const response = await _handleRequest(request);
|
|
1156
|
+
// Apply custom headers from next.config.js to non-redirect responses.
|
|
1157
|
+
// Skip redirects (3xx) because Response.redirect() creates immutable headers,
|
|
1158
|
+
// and Next.js doesn't apply custom headers to redirects anyway.
|
|
1159
|
+
if (__configHeaders.length && response && response.headers && !(response.status >= 300 && response.status < 400)) {
|
|
1160
|
+
const url = new URL(request.url);
|
|
1161
|
+
let pathname = url.pathname;
|
|
1162
|
+
${bp ? `if (pathname.startsWith(${JSON.stringify(bp)})) pathname = pathname.slice(${JSON.stringify(bp)}.length) || "/";` : ""}
|
|
1163
|
+
const extraHeaders = __applyConfigHeaders(pathname);
|
|
1164
|
+
for (const h of extraHeaders) {
|
|
1165
|
+
response.headers.set(h.key, h.value);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return response;
|
|
1169
|
+
})
|
|
1170
|
+
)
|
|
1171
|
+
)
|
|
1172
|
+
)
|
|
1173
|
+
);
|
|
1117
1174
|
}
|
|
1118
1175
|
|
|
1119
1176
|
async function _handleRequest(request) {
|
|
1120
1177
|
const url = new URL(request.url);
|
|
1121
|
-
let pathname = url.pathname;
|
|
1122
1178
|
|
|
1123
|
-
//
|
|
1179
|
+
// ── Cross-origin request protection ─────────────────────────────────
|
|
1180
|
+
// Block requests from non-localhost origins to prevent data exfiltration.
|
|
1181
|
+
const __originBlock = __validateDevRequestOrigin(request);
|
|
1182
|
+
if (__originBlock) return __originBlock;
|
|
1183
|
+
|
|
1184
|
+
// Guard against protocol-relative URL open redirects.
|
|
1124
1185
|
// Paths like //example.com/ would be redirected to //example.com by the
|
|
1125
1186
|
// trailing-slash normalizer, which browsers interpret as http://example.com.
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
|
|
1187
|
+
// Backslashes are equivalent to forward slashes in the URL spec
|
|
1188
|
+
// (e.g. /\\evil.com is treated as //evil.com by browsers and the URL constructor).
|
|
1189
|
+
// Next.js returns 404 for these paths. Check the RAW pathname before
|
|
1190
|
+
// normalization so the guard fires before normalizePath collapses //.
|
|
1191
|
+
if (url.pathname.replaceAll("\\\\", "/").startsWith("//")) {
|
|
1129
1192
|
return new Response("404 Not Found", { status: 404 });
|
|
1130
1193
|
}
|
|
1131
1194
|
|
|
1195
|
+
// Decode percent-encoding and normalize pathname to canonical form.
|
|
1196
|
+
// decodeURIComponent prevents /%61dmin from bypassing /admin matchers.
|
|
1197
|
+
// __normalizePath collapses //foo///bar → /foo/bar, resolves . and .. segments.
|
|
1198
|
+
let pathname = __normalizePath(decodeURIComponent(url.pathname));
|
|
1199
|
+
|
|
1132
1200
|
${bp ? `
|
|
1133
1201
|
// Strip basePath prefix
|
|
1134
1202
|
if (__basePath && pathname.startsWith(__basePath)) {
|
|
@@ -1151,9 +1219,11 @@ async function _handleRequest(request) {
|
|
|
1151
1219
|
if (__configRedirects.length) {
|
|
1152
1220
|
const __redir = __applyConfigRedirects(pathname, __reqCtx);
|
|
1153
1221
|
if (__redir) {
|
|
1154
|
-
const __redirDest =
|
|
1155
|
-
|
|
1156
|
-
|
|
1222
|
+
const __redirDest = __sanitizeDestination(
|
|
1223
|
+
__basePath && !__redir.destination.startsWith(__basePath)
|
|
1224
|
+
? __basePath + __redir.destination
|
|
1225
|
+
: __redir.destination
|
|
1226
|
+
);
|
|
1157
1227
|
return new Response(null, {
|
|
1158
1228
|
status: __redir.permanent ? 308 : 307,
|
|
1159
1229
|
headers: { Location: __redirDest },
|
|
@@ -1186,7 +1256,7 @@ async function _handleRequest(request) {
|
|
|
1186
1256
|
// Run proxy/middleware if present and path matches
|
|
1187
1257
|
const middlewareFn = middlewareModule.default || middlewareModule.proxy || middlewareModule.middleware;
|
|
1188
1258
|
const middlewareMatcher = middlewareModule.config?.matcher;
|
|
1189
|
-
if (typeof middlewareFn === "function" &&
|
|
1259
|
+
if (typeof middlewareFn === "function" && matchesMiddleware(cleanPathname, middlewareMatcher)) {
|
|
1190
1260
|
try {
|
|
1191
1261
|
// Wrap in NextRequest so middleware gets .nextUrl, .cookies, .geo, .ip, etc.
|
|
1192
1262
|
const nextRequest = request instanceof NextRequest ? request : new NextRequest(request);
|
|
@@ -1250,14 +1320,22 @@ async function _handleRequest(request) {
|
|
|
1250
1320
|
|
|
1251
1321
|
// ── Image optimization passthrough (dev mode — no transformation) ───────
|
|
1252
1322
|
if (cleanPathname === "/_vinext/image") {
|
|
1253
|
-
const
|
|
1323
|
+
const __rawImgUrl = url.searchParams.get("url");
|
|
1324
|
+
// Normalize backslashes: browsers and the URL constructor treat
|
|
1325
|
+
// /\\evil.com as protocol-relative (//evil.com), bypassing the // check.
|
|
1326
|
+
const __imgUrl = __rawImgUrl?.replaceAll("\\\\", "/") ?? null;
|
|
1254
1327
|
// Allowlist: must start with "/" but not "//" — blocks absolute URLs,
|
|
1255
|
-
// protocol-relative, and exotic schemes
|
|
1328
|
+
// protocol-relative, backslash variants, and exotic schemes.
|
|
1256
1329
|
if (!__imgUrl || !__imgUrl.startsWith("/") || __imgUrl.startsWith("//")) {
|
|
1257
|
-
return new Response(!
|
|
1330
|
+
return new Response(!__rawImgUrl ? "Missing url parameter" : "Only relative URLs allowed", { status: 400 });
|
|
1331
|
+
}
|
|
1332
|
+
// Validate the constructed URL's origin hasn't changed (defense in depth).
|
|
1333
|
+
const __resolvedImg = new URL(__imgUrl, request.url);
|
|
1334
|
+
if (__resolvedImg.origin !== url.origin) {
|
|
1335
|
+
return new Response("Only relative URLs allowed", { status: 400 });
|
|
1258
1336
|
}
|
|
1259
1337
|
// In dev, redirect to the original asset URL so Vite's static serving handles it.
|
|
1260
|
-
return Response.redirect(
|
|
1338
|
+
return Response.redirect(__resolvedImg.href, 302);
|
|
1261
1339
|
}
|
|
1262
1340
|
|
|
1263
1341
|
// Handle metadata routes (sitemap.xml, robots.txt, manifest.webmanifest, etc.)
|
|
@@ -1315,11 +1393,33 @@ async function _handleRequest(request) {
|
|
|
1315
1393
|
// cross-site request forgery, matching Next.js server action behavior.
|
|
1316
1394
|
const csrfResponse = __validateCsrfOrigin(request);
|
|
1317
1395
|
if (csrfResponse) return csrfResponse;
|
|
1396
|
+
|
|
1397
|
+
// ── Body size limit ─────────────────────────────────────────────────
|
|
1398
|
+
// Reject payloads larger than the configured limit.
|
|
1399
|
+
// Check Content-Length as a fast path, then enforce on the actual
|
|
1400
|
+
// stream to prevent bypasses via chunked transfer-encoding.
|
|
1401
|
+
const contentLength = parseInt(request.headers.get("content-length") || "0", 10);
|
|
1402
|
+
if (contentLength > __MAX_ACTION_BODY_SIZE) {
|
|
1403
|
+
setHeadersContext(null);
|
|
1404
|
+
setNavigationContext(null);
|
|
1405
|
+
return new Response("Payload Too Large", { status: 413 });
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1318
1408
|
try {
|
|
1319
1409
|
const contentType = request.headers.get("content-type") || "";
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1410
|
+
let body;
|
|
1411
|
+
try {
|
|
1412
|
+
body = contentType.startsWith("multipart/form-data")
|
|
1413
|
+
? await __readFormDataWithLimit(request, __MAX_ACTION_BODY_SIZE)
|
|
1414
|
+
: await __readBodyWithLimit(request, __MAX_ACTION_BODY_SIZE);
|
|
1415
|
+
} catch (sizeErr) {
|
|
1416
|
+
if (sizeErr && sizeErr.message === "Request body too large") {
|
|
1417
|
+
setHeadersContext(null);
|
|
1418
|
+
setNavigationContext(null);
|
|
1419
|
+
return new Response("Payload Too Large", { status: 413 });
|
|
1420
|
+
}
|
|
1421
|
+
throw sizeErr;
|
|
1422
|
+
}
|
|
1323
1423
|
const temporaryReferences = createTemporaryReferenceSet();
|
|
1324
1424
|
const args = await decodeReply(body, { temporaryReferences });
|
|
1325
1425
|
const action = await loadServerAction(actionId);
|
|
@@ -1331,12 +1431,14 @@ async function _handleRequest(request) {
|
|
|
1331
1431
|
} catch (e) {
|
|
1332
1432
|
// Detect redirect() / permanentRedirect() called inside the action.
|
|
1333
1433
|
// These throw errors with digest "NEXT_REDIRECT;replace;url[;status]".
|
|
1434
|
+
// The URL is encodeURIComponent-encoded to prevent semicolons in the URL
|
|
1435
|
+
// from corrupting the delimiter-based digest format.
|
|
1334
1436
|
if (e && typeof e === "object" && "digest" in e) {
|
|
1335
1437
|
const digest = String(e.digest);
|
|
1336
1438
|
if (digest.startsWith("NEXT_REDIRECT;")) {
|
|
1337
1439
|
const parts = digest.split(";");
|
|
1338
1440
|
actionRedirect = {
|
|
1339
|
-
url: parts[2],
|
|
1441
|
+
url: decodeURIComponent(parts[2]),
|
|
1340
1442
|
type: parts[1] || "replace", // "push" or "replace"
|
|
1341
1443
|
status: parts[3] ? parseInt(parts[3], 10) : 307,
|
|
1342
1444
|
};
|
|
@@ -1345,10 +1447,16 @@ async function _handleRequest(request) {
|
|
|
1345
1447
|
// notFound() / forbidden() / unauthorized() in action — package as error
|
|
1346
1448
|
returnValue = { ok: false, data: e };
|
|
1347
1449
|
} else {
|
|
1348
|
-
|
|
1450
|
+
// Non-navigation digest error — sanitize in production to avoid
|
|
1451
|
+
// leaking internal details (connection strings, paths, etc.)
|
|
1452
|
+
console.error("[vinext] Server action error:", e);
|
|
1453
|
+
returnValue = { ok: false, data: __sanitizeErrorForClient(e) };
|
|
1349
1454
|
}
|
|
1350
1455
|
} else {
|
|
1351
|
-
|
|
1456
|
+
// Unhandled error — sanitize in production to avoid leaking
|
|
1457
|
+
// internal details (database errors, file paths, stack traces, etc.)
|
|
1458
|
+
console.error("[vinext] Server action error:", e);
|
|
1459
|
+
returnValue = { ok: false, data: __sanitizeErrorForClient(e) };
|
|
1352
1460
|
}
|
|
1353
1461
|
}
|
|
1354
1462
|
|
|
@@ -1363,6 +1471,7 @@ async function _handleRequest(request) {
|
|
|
1363
1471
|
setNavigationContext(null);
|
|
1364
1472
|
const redirectHeaders = new Headers({
|
|
1365
1473
|
"Content-Type": "text/x-component; charset=utf-8",
|
|
1474
|
+
"Vary": "RSC, Accept",
|
|
1366
1475
|
"x-action-redirect": actionRedirect.url,
|
|
1367
1476
|
"x-action-redirect-type": actionRedirect.type,
|
|
1368
1477
|
"x-action-redirect-status": String(actionRedirect.status),
|
|
@@ -1402,7 +1511,7 @@ async function _handleRequest(request) {
|
|
|
1402
1511
|
setHeadersContext(null);
|
|
1403
1512
|
setNavigationContext(null);
|
|
1404
1513
|
|
|
1405
|
-
const actionHeaders = { "Content-Type": "text/x-component; charset=utf-8" };
|
|
1514
|
+
const actionHeaders = { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" };
|
|
1406
1515
|
const actionResponse = new Response(rscStream, { headers: actionHeaders });
|
|
1407
1516
|
if (actionPendingCookies.length > 0 || actionDraftCookie) {
|
|
1408
1517
|
for (const cookie of actionPendingCookies) {
|
|
@@ -1559,7 +1668,7 @@ async function _handleRequest(request) {
|
|
|
1559
1668
|
const digest = String(err.digest);
|
|
1560
1669
|
if (digest.startsWith("NEXT_REDIRECT;")) {
|
|
1561
1670
|
const parts = digest.split(";");
|
|
1562
|
-
const redirectUrl = parts[2];
|
|
1671
|
+
const redirectUrl = decodeURIComponent(parts[2]);
|
|
1563
1672
|
const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
|
|
1564
1673
|
setHeadersContext(null);
|
|
1565
1674
|
setNavigationContext(null);
|
|
@@ -1705,7 +1814,7 @@ async function _handleRequest(request) {
|
|
|
1705
1814
|
setHeadersContext(null);
|
|
1706
1815
|
setNavigationContext(null);
|
|
1707
1816
|
return new Response(interceptStream, {
|
|
1708
|
-
headers: { "Content-Type": "text/x-component; charset=utf-8" },
|
|
1817
|
+
headers: { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" },
|
|
1709
1818
|
});
|
|
1710
1819
|
}
|
|
1711
1820
|
// If sourceRoute === route, apply intercept opts to the normal render
|
|
@@ -1726,7 +1835,7 @@ async function _handleRequest(request) {
|
|
|
1726
1835
|
const digest = String(buildErr.digest);
|
|
1727
1836
|
if (digest.startsWith("NEXT_REDIRECT;")) {
|
|
1728
1837
|
const parts = digest.split(";");
|
|
1729
|
-
const redirectUrl = parts[2];
|
|
1838
|
+
const redirectUrl = decodeURIComponent(parts[2]);
|
|
1730
1839
|
const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
|
|
1731
1840
|
setHeadersContext(null);
|
|
1732
1841
|
setNavigationContext(null);
|
|
@@ -1757,7 +1866,7 @@ async function _handleRequest(request) {
|
|
|
1757
1866
|
const digest = String(err.digest);
|
|
1758
1867
|
if (digest.startsWith("NEXT_REDIRECT;")) {
|
|
1759
1868
|
const parts = digest.split(";");
|
|
1760
|
-
const redirectUrl = parts[2];
|
|
1869
|
+
const redirectUrl = decodeURIComponent(parts[2]);
|
|
1761
1870
|
const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
|
|
1762
1871
|
setHeadersContext(null);
|
|
1763
1872
|
setNavigationContext(null);
|
|
@@ -1799,13 +1908,13 @@ async function _handleRequest(request) {
|
|
|
1799
1908
|
} catch (layoutErr) {
|
|
1800
1909
|
if (layoutErr && typeof layoutErr === "object" && "digest" in layoutErr) {
|
|
1801
1910
|
const digest = String(layoutErr.digest);
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1911
|
+
if (digest.startsWith("NEXT_REDIRECT;")) {
|
|
1912
|
+
const parts = digest.split(";");
|
|
1913
|
+
const redirectUrl = decodeURIComponent(parts[2]);
|
|
1914
|
+
const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
|
|
1915
|
+
setHeadersContext(null);
|
|
1916
|
+
setNavigationContext(null);
|
|
1917
|
+
return Response.redirect(new URL(redirectUrl, request.url), statusCode);
|
|
1809
1918
|
}
|
|
1810
1919
|
if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
|
|
1811
1920
|
const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
|
|
@@ -1891,7 +2000,7 @@ async function _handleRequest(request) {
|
|
|
1891
2000
|
// The RSC stream is consumed lazily - components render when chunks are read.
|
|
1892
2001
|
// If we clear context now, headers()/cookies() will fail during rendering.
|
|
1893
2002
|
// Context will be cleared when the next request starts (via runWithHeadersContext).
|
|
1894
|
-
const responseHeaders = { "Content-Type": "text/x-component; charset=utf-8" };
|
|
2003
|
+
const responseHeaders = { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" };
|
|
1895
2004
|
// Include matched route params so the client can hydrate useParams()
|
|
1896
2005
|
if (params && Object.keys(params).length > 0) {
|
|
1897
2006
|
responseHeaders["X-Vinext-Params"] = JSON.stringify(params);
|
|
@@ -1993,6 +2102,7 @@ async function _handleRequest(request) {
|
|
|
1993
2102
|
headers: {
|
|
1994
2103
|
"Content-Type": "text/html; charset=utf-8",
|
|
1995
2104
|
"Cache-Control": "no-store, must-revalidate",
|
|
2105
|
+
"Vary": "RSC, Accept",
|
|
1996
2106
|
},
|
|
1997
2107
|
}));
|
|
1998
2108
|
}
|
|
@@ -2008,6 +2118,7 @@ async function _handleRequest(request) {
|
|
|
2008
2118
|
"Content-Type": "text/html; charset=utf-8",
|
|
2009
2119
|
"Cache-Control": "s-maxage=31536000, stale-while-revalidate",
|
|
2010
2120
|
"X-Vinext-Cache": "STATIC",
|
|
2121
|
+
"Vary": "RSC, Accept",
|
|
2011
2122
|
},
|
|
2012
2123
|
}));
|
|
2013
2124
|
}
|
|
@@ -2019,6 +2130,7 @@ async function _handleRequest(request) {
|
|
|
2019
2130
|
headers: {
|
|
2020
2131
|
"Content-Type": "text/html; charset=utf-8",
|
|
2021
2132
|
"Cache-Control": "no-store, must-revalidate",
|
|
2133
|
+
"Vary": "RSC, Accept",
|
|
2022
2134
|
},
|
|
2023
2135
|
}));
|
|
2024
2136
|
}
|
|
@@ -2030,12 +2142,13 @@ async function _handleRequest(request) {
|
|
|
2030
2142
|
headers: {
|
|
2031
2143
|
"Content-Type": "text/html; charset=utf-8",
|
|
2032
2144
|
"Cache-Control": "s-maxage=" + revalidateSeconds + ", stale-while-revalidate",
|
|
2145
|
+
"Vary": "RSC, Accept",
|
|
2033
2146
|
},
|
|
2034
2147
|
}));
|
|
2035
2148
|
}
|
|
2036
2149
|
|
|
2037
2150
|
return attachMiddlewareContext(new Response(htmlStream, {
|
|
2038
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
2151
|
+
headers: { "Content-Type": "text/html; charset=utf-8", "Vary": "RSC, Accept" },
|
|
2039
2152
|
}));
|
|
2040
2153
|
}
|
|
2041
2154
|
|
|
@@ -2055,6 +2168,7 @@ export function generateSsrEntry() {
|
|
|
2055
2168
|
import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
|
|
2056
2169
|
import { renderToReadableStream } from "react-dom/server.edge";
|
|
2057
2170
|
import { setNavigationContext } from "next/navigation";
|
|
2171
|
+
import { runWithNavigationContext as _runWithNavCtx } from "vinext/navigation-state";
|
|
2058
2172
|
import { safeJsonStringify } from "vinext/html";
|
|
2059
2173
|
|
|
2060
2174
|
/**
|
|
@@ -2180,6 +2294,10 @@ function createRscEmbedTransform(embedStream) {
|
|
|
2180
2294
|
* and the data needs to be passed to SSR since they're separate module instances.
|
|
2181
2295
|
*/
|
|
2182
2296
|
export async function handleSsr(rscStream, navContext, fontData) {
|
|
2297
|
+
// Wrap in a navigation ALS scope for per-request isolation in the SSR
|
|
2298
|
+
// environment. The SSR environment has separate module instances from RSC,
|
|
2299
|
+
// so it needs its own ALS scope.
|
|
2300
|
+
return _runWithNavCtx(async () => {
|
|
2183
2301
|
// Set navigation context so hooks like usePathname() work during SSR
|
|
2184
2302
|
// of "use client" components
|
|
2185
2303
|
if (navContext) {
|
|
@@ -2213,6 +2331,16 @@ export async function handleSsr(rscStream, navContext, fontData) {
|
|
|
2213
2331
|
const bootstrapScriptContent =
|
|
2214
2332
|
await import.meta.viteRsc.loadBootstrapScriptContent("index");
|
|
2215
2333
|
|
|
2334
|
+
// djb2 hash for digest generation in the SSR environment.
|
|
2335
|
+
// Matches the RSC environment's __errorDigest function.
|
|
2336
|
+
function ssrErrorDigest(str) {
|
|
2337
|
+
let hash = 5381;
|
|
2338
|
+
for (let i = str.length - 1; i >= 0; i--) {
|
|
2339
|
+
hash = (hash * 33) ^ str.charCodeAt(i);
|
|
2340
|
+
}
|
|
2341
|
+
return (hash >>> 0).toString();
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2216
2344
|
// Render HTML (streaming SSR)
|
|
2217
2345
|
// useServerInsertedHTML callbacks are registered during this render.
|
|
2218
2346
|
// The onError callback preserves the digest for Next.js navigation errors
|
|
@@ -2220,12 +2348,20 @@ export async function handleSsr(rscStream, navContext, fontData) {
|
|
|
2220
2348
|
// boundaries during RSC streaming. Without this, React's default onError
|
|
2221
2349
|
// returns undefined and the digest is lost in the $RX() call, preventing
|
|
2222
2350
|
// client-side error boundaries from identifying the error type.
|
|
2351
|
+
// In production, non-navigation errors also get a digest hash so they
|
|
2352
|
+
// can be correlated with server logs without leaking details to clients.
|
|
2223
2353
|
const htmlStream = await renderToReadableStream(root, {
|
|
2224
2354
|
bootstrapScriptContent,
|
|
2225
2355
|
onError(error) {
|
|
2226
2356
|
if (error && typeof error === "object" && "digest" in error) {
|
|
2227
2357
|
return String(error.digest);
|
|
2228
2358
|
}
|
|
2359
|
+
// In production, generate a digest hash for non-navigation errors
|
|
2360
|
+
if (process.env.NODE_ENV === "production" && error) {
|
|
2361
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2362
|
+
const stack = error instanceof Error ? (error.stack || "") : "";
|
|
2363
|
+
return ssrErrorDigest(msg + stack);
|
|
2364
|
+
}
|
|
2229
2365
|
return undefined;
|
|
2230
2366
|
},
|
|
2231
2367
|
});
|
|
@@ -2405,6 +2541,7 @@ export async function handleSsr(rscStream, navContext, fontData) {
|
|
|
2405
2541
|
setNavigationContext(null);
|
|
2406
2542
|
clearServerInsertedHTML();
|
|
2407
2543
|
}
|
|
2544
|
+
}); // end _runWithNavCtx
|
|
2408
2545
|
}
|
|
2409
2546
|
`;
|
|
2410
2547
|
}
|