vinext 0.0.9 → 0.0.11

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.
Files changed (97) hide show
  1. package/dist/cli.js +4 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/client/entry.js +1 -15
  4. package/dist/client/entry.js.map +1 -1
  5. package/dist/client/validate-module-path.d.ts +15 -0
  6. package/dist/client/validate-module-path.d.ts.map +1 -0
  7. package/dist/client/validate-module-path.js +31 -0
  8. package/dist/client/validate-module-path.js.map +1 -0
  9. package/dist/config/config-matchers.d.ts +20 -0
  10. package/dist/config/config-matchers.d.ts.map +1 -1
  11. package/dist/config/config-matchers.js +185 -36
  12. package/dist/config/config-matchers.js.map +1 -1
  13. package/dist/config/next-config.d.ts +4 -0
  14. package/dist/config/next-config.d.ts.map +1 -1
  15. package/dist/config/next-config.js.map +1 -1
  16. package/dist/deploy.d.ts.map +1 -1
  17. package/dist/deploy.js +20 -12
  18. package/dist/deploy.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +173 -155
  21. package/dist/index.js.map +1 -1
  22. package/dist/server/api-handler.d.ts.map +1 -1
  23. package/dist/server/api-handler.js +2 -1
  24. package/dist/server/api-handler.js.map +1 -1
  25. package/dist/server/app-dev-server.d.ts +2 -0
  26. package/dist/server/app-dev-server.d.ts.map +1 -1
  27. package/dist/server/app-dev-server.js +305 -159
  28. package/dist/server/app-dev-server.js.map +1 -1
  29. package/dist/server/app-router-entry.d.ts.map +1 -1
  30. package/dist/server/app-router-entry.js +16 -3
  31. package/dist/server/app-router-entry.js.map +1 -1
  32. package/dist/server/dev-origin-check.d.ts +61 -0
  33. package/dist/server/dev-origin-check.d.ts.map +1 -0
  34. package/dist/server/dev-origin-check.js +164 -0
  35. package/dist/server/dev-origin-check.js.map +1 -0
  36. package/dist/server/dev-server.d.ts +0 -2
  37. package/dist/server/dev-server.d.ts.map +1 -1
  38. package/dist/server/dev-server.js +390 -372
  39. package/dist/server/dev-server.js.map +1 -1
  40. package/dist/server/image-optimization.d.ts +32 -2
  41. package/dist/server/image-optimization.d.ts.map +1 -1
  42. package/dist/server/image-optimization.js +110 -9
  43. package/dist/server/image-optimization.js.map +1 -1
  44. package/dist/server/middleware-codegen.d.ts +41 -0
  45. package/dist/server/middleware-codegen.d.ts.map +1 -0
  46. package/dist/server/middleware-codegen.js +187 -0
  47. package/dist/server/middleware-codegen.js.map +1 -0
  48. package/dist/server/middleware.d.ts.map +1 -1
  49. package/dist/server/middleware.js +37 -19
  50. package/dist/server/middleware.js.map +1 -1
  51. package/dist/server/normalize-path.d.ts +22 -0
  52. package/dist/server/normalize-path.d.ts.map +1 -0
  53. package/dist/server/normalize-path.js +50 -0
  54. package/dist/server/normalize-path.js.map +1 -0
  55. package/dist/server/prod-server.d.ts.map +1 -1
  56. package/dist/server/prod-server.js +95 -26
  57. package/dist/server/prod-server.js.map +1 -1
  58. package/dist/shims/cache-runtime.d.ts +7 -0
  59. package/dist/shims/cache-runtime.d.ts.map +1 -1
  60. package/dist/shims/cache-runtime.js +19 -15
  61. package/dist/shims/cache-runtime.js.map +1 -1
  62. package/dist/shims/cache.d.ts +8 -0
  63. package/dist/shims/cache.d.ts.map +1 -1
  64. package/dist/shims/cache.js +20 -15
  65. package/dist/shims/cache.js.map +1 -1
  66. package/dist/shims/fetch-cache.d.ts +2 -3
  67. package/dist/shims/fetch-cache.d.ts.map +1 -1
  68. package/dist/shims/fetch-cache.js +80 -9
  69. package/dist/shims/fetch-cache.js.map +1 -1
  70. package/dist/shims/head-state.d.ts +6 -1
  71. package/dist/shims/head-state.d.ts.map +1 -1
  72. package/dist/shims/head-state.js +18 -15
  73. package/dist/shims/head-state.js.map +1 -1
  74. package/dist/shims/head.d.ts.map +1 -1
  75. package/dist/shims/head.js +4 -1
  76. package/dist/shims/head.js.map +1 -1
  77. package/dist/shims/headers.d.ts +9 -13
  78. package/dist/shims/headers.d.ts.map +1 -1
  79. package/dist/shims/headers.js +30 -49
  80. package/dist/shims/headers.js.map +1 -1
  81. package/dist/shims/image.d.ts.map +1 -1
  82. package/dist/shims/image.js +11 -2
  83. package/dist/shims/image.js.map +1 -1
  84. package/dist/shims/navigation-state.d.ts +6 -1
  85. package/dist/shims/navigation-state.d.ts.map +1 -1
  86. package/dist/shims/navigation-state.js +20 -29
  87. package/dist/shims/navigation-state.js.map +1 -1
  88. package/dist/shims/navigation.js +2 -2
  89. package/dist/shims/navigation.js.map +1 -1
  90. package/dist/shims/router-state.d.ts +6 -1
  91. package/dist/shims/router-state.d.ts.map +1 -1
  92. package/dist/shims/router-state.js +16 -21
  93. package/dist/shims/router-state.js.map +1 -1
  94. package/dist/shims/router.d.ts.map +1 -1
  95. package/dist/shims/router.js +19 -6
  96. package/dist/shims/router.js.map +1 -1
  97. 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, _initRequestScopedCacheState } from "next/cache";
204
+ import { _consumeRequestScopedCacheLife, _runWithCacheState } from "next/cache";
203
205
  import { runWithFetchCache } from "vinext/fetch-cache";
204
- import { clearPrivateCache as _clearPrivateCache } from "vinext/cache-runtime";
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
- // Without this, React's default onError returns undefined, the digest is lost,
231
- // and client-side error boundaries can't identify the error type.
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 errorObj = error instanceof Error ? error : new Error(String(error));
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
- function __isSafeRegex(pattern) {
847
- const quantifierAtDepth = [];
848
- let depth = 0;
849
- let i = 0;
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 handling in runWithHeadersContext to ensure
1090
- // headers() and cookies() work throughout the async RSC rendering pipeline.
1091
- // This uses AsyncLocalStorage.run() which properly propagates through awaits.
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
- return runWithHeadersContext(headersCtx, async () => {
1094
- // Initialize per-request state for cache and private cache isolation.
1095
- _initRequestScopedCacheState();
1096
- _clearPrivateCache();
1097
- // Install patched fetch with Next.js caching semantics for this request.
1098
- // runWithFetchCache uses AsyncLocalStorage.run() for proper per-request
1099
- // isolation of collected fetch tags in concurrent environments.
1100
- return runWithFetchCache(async () => {
1101
- const response = await _handleRequest(request);
1102
- // Apply custom headers from next.config.js to non-redirect responses.
1103
- // Skip redirects (3xx) because Response.redirect() creates immutable headers,
1104
- // and Next.js doesn't apply custom headers to redirects anyway.
1105
- if (__configHeaders.length && response && response.headers && !(response.status >= 300 && response.status < 400)) {
1106
- const url = new URL(request.url);
1107
- let pathname = url.pathname;
1108
- ${bp ? `if (pathname.startsWith(${JSON.stringify(bp)})) pathname = pathname.slice(${JSON.stringify(bp)}.length) || "/";` : ""}
1109
- const extraHeaders = __applyConfigHeaders(pathname);
1110
- for (const h of extraHeaders) {
1111
- response.headers.set(h.key, h.value);
1112
- }
1113
- }
1114
- return response;
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
- // Guard against protocol-relative URL open redirect attacks.
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
- // Next.js returns 404 for these paths. Check the raw pathname before any
1127
- // basePath stripping so the guard cannot be bypassed with a basePath prefix.
1128
- if (pathname.startsWith("//")) {
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 = __basePath && !__redir.destination.startsWith(__basePath)
1155
- ? __basePath + __redir.destination
1156
- : __redir.destination;
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,10 +1256,18 @@ 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" && matchMiddlewarePath(cleanPathname, middlewareMatcher)) {
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
- const nextRequest = request instanceof NextRequest ? request : new NextRequest(request);
1262
+ // Strip .rsc suffix from the URL it's an internal transport detail that
1263
+ // middleware should never see (matches Next.js behavior).
1264
+ let mwRequest = request;
1265
+ if (isRscRequest && pathname.endsWith(".rsc")) {
1266
+ const mwUrl = new URL(request.url);
1267
+ mwUrl.pathname = cleanPathname;
1268
+ mwRequest = new Request(mwUrl, request);
1269
+ }
1270
+ const nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
1193
1271
  const mwResponse = await middlewareFn(nextRequest);
1194
1272
  if (mwResponse) {
1195
1273
  // Check for x-middleware-next (continue)
@@ -1236,12 +1314,13 @@ async function _handleRequest(request) {
1236
1314
 
1237
1315
  // Unpack x-middleware-request-* headers into the request context so that
1238
1316
  // headers() returns the middleware-modified headers instead of the original
1239
- // request headers. Also strip those internal headers from the set that will
1240
- // be merged into the outgoing HTTP response.
1317
+ // request headers. Strip ALL x-middleware-* headers from the set that will
1318
+ // be merged into the outgoing HTTP response — this prefix is reserved for
1319
+ // internal routing signals and must never reach clients.
1241
1320
  if (_middlewareResponseHeaders) {
1242
1321
  applyMiddlewareRequestHeaders(_middlewareResponseHeaders);
1243
1322
  for (const key of [..._middlewareResponseHeaders.keys()]) {
1244
- if (key.startsWith("x-middleware-request-")) {
1323
+ if (key.startsWith("x-middleware-")) {
1245
1324
  _middlewareResponseHeaders.delete(key);
1246
1325
  }
1247
1326
  }
@@ -1250,14 +1329,22 @@ async function _handleRequest(request) {
1250
1329
 
1251
1330
  // ── Image optimization passthrough (dev mode — no transformation) ───────
1252
1331
  if (cleanPathname === "/_vinext/image") {
1253
- const __imgUrl = url.searchParams.get("url");
1332
+ const __rawImgUrl = url.searchParams.get("url");
1333
+ // Normalize backslashes: browsers and the URL constructor treat
1334
+ // /\\evil.com as protocol-relative (//evil.com), bypassing the // check.
1335
+ const __imgUrl = __rawImgUrl?.replaceAll("\\\\", "/") ?? null;
1254
1336
  // Allowlist: must start with "/" but not "//" — blocks absolute URLs,
1255
- // protocol-relative, and exotic schemes (data:, javascript:, etc.).
1337
+ // protocol-relative, backslash variants, and exotic schemes.
1256
1338
  if (!__imgUrl || !__imgUrl.startsWith("/") || __imgUrl.startsWith("//")) {
1257
- return new Response(!__imgUrl ? "Missing url parameter" : "Only relative URLs allowed", { status: 400 });
1339
+ return new Response(!__rawImgUrl ? "Missing url parameter" : "Only relative URLs allowed", { status: 400 });
1340
+ }
1341
+ // Validate the constructed URL's origin hasn't changed (defense in depth).
1342
+ const __resolvedImg = new URL(__imgUrl, request.url);
1343
+ if (__resolvedImg.origin !== url.origin) {
1344
+ return new Response("Only relative URLs allowed", { status: 400 });
1258
1345
  }
1259
1346
  // In dev, redirect to the original asset URL so Vite's static serving handles it.
1260
- return Response.redirect(new URL(__imgUrl, request.url).href, 302);
1347
+ return Response.redirect(__resolvedImg.href, 302);
1261
1348
  }
1262
1349
 
1263
1350
  // Handle metadata routes (sitemap.xml, robots.txt, manifest.webmanifest, etc.)
@@ -1315,11 +1402,33 @@ async function _handleRequest(request) {
1315
1402
  // cross-site request forgery, matching Next.js server action behavior.
1316
1403
  const csrfResponse = __validateCsrfOrigin(request);
1317
1404
  if (csrfResponse) return csrfResponse;
1405
+
1406
+ // ── Body size limit ─────────────────────────────────────────────────
1407
+ // Reject payloads larger than the configured limit.
1408
+ // Check Content-Length as a fast path, then enforce on the actual
1409
+ // stream to prevent bypasses via chunked transfer-encoding.
1410
+ const contentLength = parseInt(request.headers.get("content-length") || "0", 10);
1411
+ if (contentLength > __MAX_ACTION_BODY_SIZE) {
1412
+ setHeadersContext(null);
1413
+ setNavigationContext(null);
1414
+ return new Response("Payload Too Large", { status: 413 });
1415
+ }
1416
+
1318
1417
  try {
1319
1418
  const contentType = request.headers.get("content-type") || "";
1320
- const body = contentType.startsWith("multipart/form-data")
1321
- ? await request.formData()
1322
- : await request.text();
1419
+ let body;
1420
+ try {
1421
+ body = contentType.startsWith("multipart/form-data")
1422
+ ? await __readFormDataWithLimit(request, __MAX_ACTION_BODY_SIZE)
1423
+ : await __readBodyWithLimit(request, __MAX_ACTION_BODY_SIZE);
1424
+ } catch (sizeErr) {
1425
+ if (sizeErr && sizeErr.message === "Request body too large") {
1426
+ setHeadersContext(null);
1427
+ setNavigationContext(null);
1428
+ return new Response("Payload Too Large", { status: 413 });
1429
+ }
1430
+ throw sizeErr;
1431
+ }
1323
1432
  const temporaryReferences = createTemporaryReferenceSet();
1324
1433
  const args = await decodeReply(body, { temporaryReferences });
1325
1434
  const action = await loadServerAction(actionId);
@@ -1331,12 +1440,14 @@ async function _handleRequest(request) {
1331
1440
  } catch (e) {
1332
1441
  // Detect redirect() / permanentRedirect() called inside the action.
1333
1442
  // These throw errors with digest "NEXT_REDIRECT;replace;url[;status]".
1443
+ // The URL is encodeURIComponent-encoded to prevent semicolons in the URL
1444
+ // from corrupting the delimiter-based digest format.
1334
1445
  if (e && typeof e === "object" && "digest" in e) {
1335
1446
  const digest = String(e.digest);
1336
1447
  if (digest.startsWith("NEXT_REDIRECT;")) {
1337
1448
  const parts = digest.split(";");
1338
1449
  actionRedirect = {
1339
- url: parts[2],
1450
+ url: decodeURIComponent(parts[2]),
1340
1451
  type: parts[1] || "replace", // "push" or "replace"
1341
1452
  status: parts[3] ? parseInt(parts[3], 10) : 307,
1342
1453
  };
@@ -1345,10 +1456,16 @@ async function _handleRequest(request) {
1345
1456
  // notFound() / forbidden() / unauthorized() in action — package as error
1346
1457
  returnValue = { ok: false, data: e };
1347
1458
  } else {
1348
- returnValue = { ok: false, data: e };
1459
+ // Non-navigation digest error sanitize in production to avoid
1460
+ // leaking internal details (connection strings, paths, etc.)
1461
+ console.error("[vinext] Server action error:", e);
1462
+ returnValue = { ok: false, data: __sanitizeErrorForClient(e) };
1349
1463
  }
1350
1464
  } else {
1351
- returnValue = { ok: false, data: e };
1465
+ // Unhandled error sanitize in production to avoid leaking
1466
+ // internal details (database errors, file paths, stack traces, etc.)
1467
+ console.error("[vinext] Server action error:", e);
1468
+ returnValue = { ok: false, data: __sanitizeErrorForClient(e) };
1352
1469
  }
1353
1470
  }
1354
1471
 
@@ -1363,6 +1480,7 @@ async function _handleRequest(request) {
1363
1480
  setNavigationContext(null);
1364
1481
  const redirectHeaders = new Headers({
1365
1482
  "Content-Type": "text/x-component; charset=utf-8",
1483
+ "Vary": "RSC, Accept",
1366
1484
  "x-action-redirect": actionRedirect.url,
1367
1485
  "x-action-redirect-type": actionRedirect.type,
1368
1486
  "x-action-redirect-status": String(actionRedirect.status),
@@ -1402,7 +1520,7 @@ async function _handleRequest(request) {
1402
1520
  setHeadersContext(null);
1403
1521
  setNavigationContext(null);
1404
1522
 
1405
- const actionHeaders = { "Content-Type": "text/x-component; charset=utf-8" };
1523
+ const actionHeaders = { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" };
1406
1524
  const actionResponse = new Response(rscStream, { headers: actionHeaders });
1407
1525
  if (actionPendingCookies.length > 0 || actionDraftCookie) {
1408
1526
  for (const cookie of actionPendingCookies) {
@@ -1559,7 +1677,7 @@ async function _handleRequest(request) {
1559
1677
  const digest = String(err.digest);
1560
1678
  if (digest.startsWith("NEXT_REDIRECT;")) {
1561
1679
  const parts = digest.split(";");
1562
- const redirectUrl = parts[2];
1680
+ const redirectUrl = decodeURIComponent(parts[2]);
1563
1681
  const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
1564
1682
  setHeadersContext(null);
1565
1683
  setNavigationContext(null);
@@ -1705,7 +1823,7 @@ async function _handleRequest(request) {
1705
1823
  setHeadersContext(null);
1706
1824
  setNavigationContext(null);
1707
1825
  return new Response(interceptStream, {
1708
- headers: { "Content-Type": "text/x-component; charset=utf-8" },
1826
+ headers: { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" },
1709
1827
  });
1710
1828
  }
1711
1829
  // If sourceRoute === route, apply intercept opts to the normal render
@@ -1726,7 +1844,7 @@ async function _handleRequest(request) {
1726
1844
  const digest = String(buildErr.digest);
1727
1845
  if (digest.startsWith("NEXT_REDIRECT;")) {
1728
1846
  const parts = digest.split(";");
1729
- const redirectUrl = parts[2];
1847
+ const redirectUrl = decodeURIComponent(parts[2]);
1730
1848
  const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
1731
1849
  setHeadersContext(null);
1732
1850
  setNavigationContext(null);
@@ -1757,7 +1875,7 @@ async function _handleRequest(request) {
1757
1875
  const digest = String(err.digest);
1758
1876
  if (digest.startsWith("NEXT_REDIRECT;")) {
1759
1877
  const parts = digest.split(";");
1760
- const redirectUrl = parts[2];
1878
+ const redirectUrl = decodeURIComponent(parts[2]);
1761
1879
  const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
1762
1880
  setHeadersContext(null);
1763
1881
  setNavigationContext(null);
@@ -1799,13 +1917,13 @@ async function _handleRequest(request) {
1799
1917
  } catch (layoutErr) {
1800
1918
  if (layoutErr && typeof layoutErr === "object" && "digest" in layoutErr) {
1801
1919
  const digest = String(layoutErr.digest);
1802
- if (digest.startsWith("NEXT_REDIRECT;")) {
1803
- const parts = digest.split(";");
1804
- const redirectUrl = parts[2];
1805
- const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
1806
- setHeadersContext(null);
1807
- setNavigationContext(null);
1808
- return Response.redirect(new URL(redirectUrl, request.url), statusCode);
1920
+ if (digest.startsWith("NEXT_REDIRECT;")) {
1921
+ const parts = digest.split(";");
1922
+ const redirectUrl = decodeURIComponent(parts[2]);
1923
+ const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
1924
+ setHeadersContext(null);
1925
+ setNavigationContext(null);
1926
+ return Response.redirect(new URL(redirectUrl, request.url), statusCode);
1809
1927
  }
1810
1928
  if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
1811
1929
  const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
@@ -1891,7 +2009,7 @@ async function _handleRequest(request) {
1891
2009
  // The RSC stream is consumed lazily - components render when chunks are read.
1892
2010
  // If we clear context now, headers()/cookies() will fail during rendering.
1893
2011
  // Context will be cleared when the next request starts (via runWithHeadersContext).
1894
- const responseHeaders = { "Content-Type": "text/x-component; charset=utf-8" };
2012
+ const responseHeaders = { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" };
1895
2013
  // Include matched route params so the client can hydrate useParams()
1896
2014
  if (params && Object.keys(params).length > 0) {
1897
2015
  responseHeaders["X-Vinext-Params"] = JSON.stringify(params);
@@ -1993,6 +2111,7 @@ async function _handleRequest(request) {
1993
2111
  headers: {
1994
2112
  "Content-Type": "text/html; charset=utf-8",
1995
2113
  "Cache-Control": "no-store, must-revalidate",
2114
+ "Vary": "RSC, Accept",
1996
2115
  },
1997
2116
  }));
1998
2117
  }
@@ -2008,6 +2127,7 @@ async function _handleRequest(request) {
2008
2127
  "Content-Type": "text/html; charset=utf-8",
2009
2128
  "Cache-Control": "s-maxage=31536000, stale-while-revalidate",
2010
2129
  "X-Vinext-Cache": "STATIC",
2130
+ "Vary": "RSC, Accept",
2011
2131
  },
2012
2132
  }));
2013
2133
  }
@@ -2019,6 +2139,7 @@ async function _handleRequest(request) {
2019
2139
  headers: {
2020
2140
  "Content-Type": "text/html; charset=utf-8",
2021
2141
  "Cache-Control": "no-store, must-revalidate",
2142
+ "Vary": "RSC, Accept",
2022
2143
  },
2023
2144
  }));
2024
2145
  }
@@ -2030,12 +2151,13 @@ async function _handleRequest(request) {
2030
2151
  headers: {
2031
2152
  "Content-Type": "text/html; charset=utf-8",
2032
2153
  "Cache-Control": "s-maxage=" + revalidateSeconds + ", stale-while-revalidate",
2154
+ "Vary": "RSC, Accept",
2033
2155
  },
2034
2156
  }));
2035
2157
  }
2036
2158
 
2037
2159
  return attachMiddlewareContext(new Response(htmlStream, {
2038
- headers: { "Content-Type": "text/html; charset=utf-8" },
2160
+ headers: { "Content-Type": "text/html; charset=utf-8", "Vary": "RSC, Accept" },
2039
2161
  }));
2040
2162
  }
2041
2163
 
@@ -2055,6 +2177,7 @@ export function generateSsrEntry() {
2055
2177
  import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
2056
2178
  import { renderToReadableStream } from "react-dom/server.edge";
2057
2179
  import { setNavigationContext } from "next/navigation";
2180
+ import { runWithNavigationContext as _runWithNavCtx } from "vinext/navigation-state";
2058
2181
  import { safeJsonStringify } from "vinext/html";
2059
2182
 
2060
2183
  /**
@@ -2180,6 +2303,10 @@ function createRscEmbedTransform(embedStream) {
2180
2303
  * and the data needs to be passed to SSR since they're separate module instances.
2181
2304
  */
2182
2305
  export async function handleSsr(rscStream, navContext, fontData) {
2306
+ // Wrap in a navigation ALS scope for per-request isolation in the SSR
2307
+ // environment. The SSR environment has separate module instances from RSC,
2308
+ // so it needs its own ALS scope.
2309
+ return _runWithNavCtx(async () => {
2183
2310
  // Set navigation context so hooks like usePathname() work during SSR
2184
2311
  // of "use client" components
2185
2312
  if (navContext) {
@@ -2213,6 +2340,16 @@ export async function handleSsr(rscStream, navContext, fontData) {
2213
2340
  const bootstrapScriptContent =
2214
2341
  await import.meta.viteRsc.loadBootstrapScriptContent("index");
2215
2342
 
2343
+ // djb2 hash for digest generation in the SSR environment.
2344
+ // Matches the RSC environment's __errorDigest function.
2345
+ function ssrErrorDigest(str) {
2346
+ let hash = 5381;
2347
+ for (let i = str.length - 1; i >= 0; i--) {
2348
+ hash = (hash * 33) ^ str.charCodeAt(i);
2349
+ }
2350
+ return (hash >>> 0).toString();
2351
+ }
2352
+
2216
2353
  // Render HTML (streaming SSR)
2217
2354
  // useServerInsertedHTML callbacks are registered during this render.
2218
2355
  // The onError callback preserves the digest for Next.js navigation errors
@@ -2220,12 +2357,20 @@ export async function handleSsr(rscStream, navContext, fontData) {
2220
2357
  // boundaries during RSC streaming. Without this, React's default onError
2221
2358
  // returns undefined and the digest is lost in the $RX() call, preventing
2222
2359
  // client-side error boundaries from identifying the error type.
2360
+ // In production, non-navigation errors also get a digest hash so they
2361
+ // can be correlated with server logs without leaking details to clients.
2223
2362
  const htmlStream = await renderToReadableStream(root, {
2224
2363
  bootstrapScriptContent,
2225
2364
  onError(error) {
2226
2365
  if (error && typeof error === "object" && "digest" in error) {
2227
2366
  return String(error.digest);
2228
2367
  }
2368
+ // In production, generate a digest hash for non-navigation errors
2369
+ if (process.env.NODE_ENV === "production" && error) {
2370
+ const msg = error instanceof Error ? error.message : String(error);
2371
+ const stack = error instanceof Error ? (error.stack || "") : "";
2372
+ return ssrErrorDigest(msg + stack);
2373
+ }
2229
2374
  return undefined;
2230
2375
  },
2231
2376
  });
@@ -2405,6 +2550,7 @@ export async function handleSsr(rscStream, navContext, fontData) {
2405
2550
  setNavigationContext(null);
2406
2551
  clearServerInsertedHTML();
2407
2552
  }
2553
+ }); // end _runWithNavCtx
2408
2554
  }
2409
2555
  `;
2410
2556
  }