@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2154 -861
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +243 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +128 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +121 -0
- package/skills/testing/e2e-parity.md +124 -0
- package/skills/testing/flight.md +89 -0
- package/skills/testing/handles.md +127 -0
- package/skills/testing/loader.md +108 -0
- package/skills/testing/middleware.md +97 -0
- package/skills/testing/render-handler.md +102 -0
- package/skills/testing/response-routes.md +94 -0
- package/skills/testing/reverse-and-types.md +83 -0
- package/skills/testing/server-actions.md +89 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +104 -68
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +183 -44
- package/src/browser/prefetch/fetch.ts +228 -37
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +32 -1
- package/src/browser/rsc-router.tsx +69 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +8 -1
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +95 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +96 -205
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -4
- package/src/handle.ts +32 -14
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -21
- package/src/index.rsc.ts +10 -6
- package/src/index.ts +54 -17
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +25 -7
- package/src/loader.ts +16 -9
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +27 -6
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +54 -6
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +41 -22
- package/src/router/loader-resolution.ts +82 -36
- package/src/router/manifest.ts +41 -19
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +116 -19
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +40 -16
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +52 -30
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +57 -61
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +175 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +67 -51
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +25 -3
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +326 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +110 -0
- package/src/testing/flight-normalize.ts +38 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +51 -0
- package/src/testing/flight.ts +234 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +106 -0
- package/src/testing/internal/context.ts +304 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +42 -0
- package/src/testing/render-handler.ts +323 -0
- package/src/testing/render-route.tsx +590 -0
- package/src/testing/run-loader.ts +363 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +285 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +1 -5
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +106 -75
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +8 -59
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
package/src/rsc/helpers.ts
CHANGED
|
@@ -8,8 +8,67 @@ import {
|
|
|
8
8
|
_getRequestContext,
|
|
9
9
|
getLocationState,
|
|
10
10
|
} from "../server/request-context.js";
|
|
11
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
11
12
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
13
|
+
import { isRedirectResponse } from "../response-utils.js";
|
|
12
14
|
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
15
|
+
import { formatCacheSignalHeader } from "../router/telemetry.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
|
|
19
|
+
* match/matchPartial populate ctx._cacheSignal. Emit it as the X-Rango-Cache
|
|
20
|
+
* header. When the gate is off, ctx._cacheSignal is undefined and NOTHING is
|
|
21
|
+
* attached — output is byte-identical to the default. Header mutation failures
|
|
22
|
+
* are swallowed so immutable Response headers (e.g. protocol-switch) are safe.
|
|
23
|
+
*/
|
|
24
|
+
function applyCacheSignalHeader(target: Headers, ctx: RequestContext): void {
|
|
25
|
+
const signal = ctx._cacheSignal;
|
|
26
|
+
if (!signal || signal.length === 0) return;
|
|
27
|
+
try {
|
|
28
|
+
target.set("X-Rango-Cache", formatCacheSignalHeader(signal));
|
|
29
|
+
} catch {
|
|
30
|
+
// Headers immutable — skip.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Copy stub headers from the request context onto a target Headers instance:
|
|
36
|
+
* append Set-Cookie entries, set everything else only if absent. Header
|
|
37
|
+
* mutation failures are swallowed so the same logic works against Response
|
|
38
|
+
* headers that may be immutable (e.g. Cloudflare protocol-switch responses).
|
|
39
|
+
*/
|
|
40
|
+
function applyStubHeaders(target: Headers, stub: Headers): void {
|
|
41
|
+
stub.forEach((value, name) => {
|
|
42
|
+
try {
|
|
43
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
44
|
+
target.append(name, value);
|
|
45
|
+
} else if (!target.has(name)) {
|
|
46
|
+
target.set(name, value);
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Headers immutable — skip.
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Drain ctx._onResponseCallbacks onto a response. Swapping the array before
|
|
56
|
+
* iteration prevents re-entrant registrations from double-firing and matches
|
|
57
|
+
* the contract that each callback runs at most once per request.
|
|
58
|
+
*/
|
|
59
|
+
function drainOnResponseCallbacks(
|
|
60
|
+
ctx: RequestContext,
|
|
61
|
+
response: Response,
|
|
62
|
+
): Response {
|
|
63
|
+
const callbacks = ctx._onResponseCallbacks;
|
|
64
|
+
if (callbacks.length === 0) return response;
|
|
65
|
+
ctx._onResponseCallbacks = [];
|
|
66
|
+
let result = response;
|
|
67
|
+
for (const callback of callbacks) {
|
|
68
|
+
result = callback(result) ?? result;
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
13
72
|
|
|
14
73
|
/**
|
|
15
74
|
* Check if a request body has content to decode
|
|
@@ -39,40 +98,24 @@ export function createResponseWithMergedHeaders(
|
|
|
39
98
|
return new Response(body, init);
|
|
40
99
|
}
|
|
41
100
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// merge points (e.g. executeMiddleware) do not duplicate them.
|
|
101
|
+
// Delete Set-Cookie from the stub after consuming so downstream merge
|
|
102
|
+
// points (e.g. executeMiddleware) don't duplicate them.
|
|
45
103
|
const mergedHeaders = new Headers(init.headers);
|
|
46
|
-
ctx.res.headers
|
|
47
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
48
|
-
mergedHeaders.append(name, value);
|
|
49
|
-
} else if (!mergedHeaders.has(name)) {
|
|
50
|
-
// Only set if not already present in init.headers
|
|
51
|
-
mergedHeaders.set(name, value);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
104
|
+
applyStubHeaders(mergedHeaders, ctx.res.headers);
|
|
54
105
|
ctx.res.headers.delete("set-cookie");
|
|
106
|
+
applyCacheSignalHeader(mergedHeaders, ctx);
|
|
55
107
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
108
|
+
// ctx.res.status overrides init.status when explicitly set (e.g. 404 for
|
|
109
|
+
// notFound, 500 for error). Default ctx.res.status is 200.
|
|
58
110
|
const status = ctx.res.status !== 200 ? ctx.res.status : init.status;
|
|
59
111
|
|
|
60
|
-
|
|
112
|
+
const response = new Response(body, {
|
|
61
113
|
...init,
|
|
62
114
|
status,
|
|
63
115
|
headers: mergedHeaders,
|
|
64
116
|
});
|
|
65
117
|
|
|
66
|
-
|
|
67
|
-
// Drain the array so that downstream callers (e.g. finalizeResponse)
|
|
68
|
-
// do not re-execute the same callbacks on this response.
|
|
69
|
-
const callbacks = ctx._onResponseCallbacks;
|
|
70
|
-
ctx._onResponseCallbacks = [];
|
|
71
|
-
for (const callback of callbacks) {
|
|
72
|
-
response = callback(response) ?? response;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return response;
|
|
118
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
76
119
|
}
|
|
77
120
|
|
|
78
121
|
/**
|
|
@@ -122,10 +165,10 @@ export function interceptRedirectForPartial(
|
|
|
122
165
|
locationState?: Record<string, unknown>,
|
|
123
166
|
) => Response,
|
|
124
167
|
): Response | null {
|
|
125
|
-
|
|
126
|
-
if (!(response.status >= 300 && response.status < 400 && redirectUrl)) {
|
|
168
|
+
if (!isRedirectResponse(response)) {
|
|
127
169
|
return null;
|
|
128
170
|
}
|
|
171
|
+
const redirectUrl = response.headers.get("Location")!;
|
|
129
172
|
const locationState = getLocationState();
|
|
130
173
|
let intercepted: Response;
|
|
131
174
|
if (locationState) {
|
|
@@ -175,24 +218,29 @@ export function buildRouteMiddlewareEntries<TEnv>(
|
|
|
175
218
|
}
|
|
176
219
|
|
|
177
220
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
221
|
+
* Merge stub headers from the request context onto an existing Response in
|
|
222
|
+
* place, then drain onResponse callbacks. Used when a Response cannot flow
|
|
223
|
+
* through `new Response()` — status 101 is outside the constructor's
|
|
224
|
+
* 200-599 range, and the Cloudflare-specific `webSocket` property would be
|
|
225
|
+
* lost on reconstruction.
|
|
183
226
|
*/
|
|
184
|
-
export function
|
|
227
|
+
export function mergeStubHeadersAndFinalize(response: Response): Response {
|
|
185
228
|
const ctx = _getRequestContext();
|
|
186
|
-
if (!ctx
|
|
187
|
-
return response;
|
|
188
|
-
}
|
|
229
|
+
if (!ctx) return response;
|
|
189
230
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
231
|
+
applyStubHeaders(response.headers, ctx.res.headers);
|
|
232
|
+
ctx.res.headers.delete("set-cookie");
|
|
233
|
+
|
|
234
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Run onResponse callbacks on an existing Response. Used by code paths that
|
|
239
|
+
* bypass createResponseWithMergedHeaders (e.g. middleware short-circuits)
|
|
240
|
+
* but still need ctx.onResponse() callbacks to fire.
|
|
241
|
+
*/
|
|
242
|
+
export function finalizeResponse(response: Response): Response {
|
|
243
|
+
const ctx = _getRequestContext();
|
|
244
|
+
if (!ctx) return response;
|
|
245
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
198
246
|
}
|
package/src/rsc/index.ts
CHANGED
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
setRouteTrie,
|
|
14
14
|
setRouterManifest,
|
|
15
15
|
setRouterTrie,
|
|
16
|
+
setRouterPrecomputedEntries,
|
|
16
17
|
} from "../route-map-builder.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -36,47 +37,13 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
36
37
|
undefined,
|
|
37
38
|
router.basename ? { urlPrefix: router.basename } : undefined,
|
|
38
39
|
);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const routeToStaticPrefix: Record<string, string> = {};
|
|
47
|
-
for (const name of Object.keys(generated.routeManifest)) {
|
|
48
|
-
routeToStaticPrefix[name] = "";
|
|
49
|
-
}
|
|
50
|
-
// Override with prefix from include() entries so the trie
|
|
51
|
-
// returns the correct sp for lazy entry lookup in findMatch.
|
|
52
|
-
// Walk recursively to include routes in nested includes.
|
|
53
|
-
if (generated.prefixTree) {
|
|
54
|
-
const visitPrefixNode = (node: any): void => {
|
|
55
|
-
const sp = node.staticPrefix || "";
|
|
56
|
-
for (const route of node.routes || []) {
|
|
57
|
-
routeToStaticPrefix[route] = sp;
|
|
58
|
-
}
|
|
59
|
-
for (const child of Object.values(node.children || {})) {
|
|
60
|
-
visitPrefixNode(child);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
for (const node of Object.values(generated.prefixTree)) {
|
|
64
|
-
visitPrefixNode(node);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const trie = buildRouteTrie(
|
|
68
|
-
generated.routeManifest,
|
|
69
|
-
generated._routeAncestry,
|
|
70
|
-
routeToStaticPrefix,
|
|
71
|
-
generated.routeTrailingSlash,
|
|
72
|
-
generated.prerenderRoutes
|
|
73
|
-
? new Set(generated.prerenderRoutes)
|
|
74
|
-
: undefined,
|
|
75
|
-
generated.passthroughRoutes
|
|
76
|
-
? new Set(generated.passthroughRoutes)
|
|
77
|
-
: undefined,
|
|
78
|
-
generated.responseTypeRoutes,
|
|
79
|
-
);
|
|
40
|
+
// Build the trie through the SAME shared helper the production discovery uses
|
|
41
|
+
// (discover-routers.ts), so the dev runtime-rebuilt trie and the prod
|
|
42
|
+
// serialized trie cannot drift. buildPerRouterTrie returns null when there
|
|
43
|
+
// are no routes.
|
|
44
|
+
const { buildPerRouterTrie } = await import("../build/route-trie.js");
|
|
45
|
+
const trie = buildPerRouterTrie(generated);
|
|
46
|
+
if (trie) {
|
|
80
47
|
setRouterTrie(router.id, trie);
|
|
81
48
|
// Set global trie only if not already set by another router
|
|
82
49
|
if (!getRouteTrie()) {
|
|
@@ -84,6 +51,26 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
84
51
|
}
|
|
85
52
|
}
|
|
86
53
|
setRouterManifest(router.id, generated.routeManifest);
|
|
54
|
+
|
|
55
|
+
// Match the production discovery path: precompute leaf-include entries so the
|
|
56
|
+
// match-time shortcut in evaluateLazyEntry applies in dev/Cloudflare too.
|
|
57
|
+
// Without this, dev re-runs each matched leaf include's handler at match time
|
|
58
|
+
// (evaluateLazyEntry) AND again at render time (loadManifest); with it, the
|
|
59
|
+
// match-time run is skipped and the handler runs once per first request.
|
|
60
|
+
// Identical route ownership to the handler path (the shortcut is guarded by
|
|
61
|
+
// the same prefixIsShared and #506 checks production uses).
|
|
62
|
+
const { flattenLeafEntries } = await import("../build/prefix-tree-utils.js");
|
|
63
|
+
const precomputed: Array<{
|
|
64
|
+
staticPrefix: string;
|
|
65
|
+
routes: Record<string, string>;
|
|
66
|
+
}> = [];
|
|
67
|
+
flattenLeafEntries(
|
|
68
|
+
generated.prefixTree,
|
|
69
|
+
generated.routeManifest,
|
|
70
|
+
precomputed,
|
|
71
|
+
);
|
|
72
|
+
setRouterPrecomputedEntries(router.id, precomputed);
|
|
73
|
+
|
|
87
74
|
// Merge into global manifest (needed for reverse/href across routers)
|
|
88
75
|
const existing = hasCachedManifest() ? getGlobalRouteMap() : {};
|
|
89
76
|
setCachedManifest({ ...existing, ...generated.routeManifest });
|
package/src/rsc/origin-guard.ts
CHANGED
|
@@ -9,11 +9,29 @@
|
|
|
9
9
|
* navigations, bookmarks, and non-browser clients don't send Origin.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { RequestPlan } from "../router/request-classification.js";
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Request phase that triggered the origin check.
|
|
14
16
|
*/
|
|
15
17
|
export type OriginCheckPhase = "action" | "loader" | "pe-form";
|
|
16
18
|
|
|
19
|
+
// Exhaustive over RequestPlan modes so a new mode must be classified here (the
|
|
20
|
+
// security gate) instead of silently falling through to no origin check.
|
|
21
|
+
export const ORIGIN_CHECK_PHASE_BY_MODE: Record<
|
|
22
|
+
RequestPlan["mode"],
|
|
23
|
+
OriginCheckPhase | null
|
|
24
|
+
> = {
|
|
25
|
+
action: "action",
|
|
26
|
+
loader: "loader",
|
|
27
|
+
"pe-render": "pe-form",
|
|
28
|
+
"full-render": null,
|
|
29
|
+
"partial-render": null,
|
|
30
|
+
response: null,
|
|
31
|
+
redirect: null,
|
|
32
|
+
"version-mismatch": null,
|
|
33
|
+
};
|
|
34
|
+
|
|
17
35
|
/**
|
|
18
36
|
* Context passed to the originCheck callback.
|
|
19
37
|
*/
|
|
@@ -116,14 +134,15 @@ export async function checkRequestOrigin<TEnv = any>(
|
|
|
116
134
|
// Disabled by explicit opt-out
|
|
117
135
|
if (config === false) return null;
|
|
118
136
|
|
|
119
|
-
// Default
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
// Default (true/undefined) becomes a callback returning boolean, so the
|
|
138
|
+
// Response|true|reject resolution below is written once.
|
|
139
|
+
const check: (
|
|
140
|
+
ctx: OriginCheckContext<TEnv>,
|
|
141
|
+
) => boolean | Response | Promise<boolean | Response> =
|
|
142
|
+
config === true || config === undefined
|
|
143
|
+
? () => defaultOriginCheck(request, url)
|
|
144
|
+
: config;
|
|
125
145
|
|
|
126
|
-
// Custom function — build context and call
|
|
127
146
|
const ctx: OriginCheckContext<TEnv> = {
|
|
128
147
|
request,
|
|
129
148
|
url,
|
|
@@ -133,9 +152,8 @@ export async function checkRequestOrigin<TEnv = any>(
|
|
|
133
152
|
defaultCheck: () => defaultOriginCheck(request, url),
|
|
134
153
|
};
|
|
135
154
|
|
|
136
|
-
const result = await
|
|
155
|
+
const result = await check(ctx);
|
|
137
156
|
|
|
138
157
|
if (result instanceof Response) return result;
|
|
139
|
-
|
|
140
|
-
return createForbiddenResponse(request);
|
|
158
|
+
return result === true ? null : createForbiddenResponse(request);
|
|
141
159
|
}
|
|
@@ -248,6 +248,8 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
248
248
|
segments: match.segments,
|
|
249
249
|
matched: match.matched,
|
|
250
250
|
diff: match.diff,
|
|
251
|
+
resolvedIds: match.resolvedIds,
|
|
252
|
+
params: match.params,
|
|
251
253
|
isPartial: false,
|
|
252
254
|
rootLayout: ctx.router.rootLayout,
|
|
253
255
|
handles: handleStore.stream(),
|
|
@@ -353,6 +355,8 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
353
355
|
segments: errorResult.segments,
|
|
354
356
|
matched: errorResult.matched,
|
|
355
357
|
diff: errorResult.diff,
|
|
358
|
+
resolvedIds: errorResult.resolvedIds,
|
|
359
|
+
params: errorResult.params,
|
|
356
360
|
isPartial: false,
|
|
357
361
|
isError: true,
|
|
358
362
|
rootLayout: ctx.router.rootLayout,
|
|
@@ -1,37 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Problem Details (RFC 9457) Builder
|
|
3
3
|
*
|
|
4
|
-
* Builds a
|
|
5
|
-
*
|
|
4
|
+
* Builds a problem+json error body from a caught error, controlling what
|
|
5
|
+
* information is exposed based on error type and environment.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { RouterError } from "../errors.js";
|
|
9
|
-
import type {
|
|
9
|
+
import type { ProblemDetails } from "../urls.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* HTTP reason phrases for the problem `title` member. Inlined because the
|
|
13
|
+
* router targets edge/worker runtimes without node's `http.STATUS_CODES`;
|
|
14
|
+
* covers the full standard 4xx/5xx range, with a generic fallback for any
|
|
15
|
+
* non-standard status a handler might set.
|
|
16
|
+
*/
|
|
17
|
+
const STATUS_PHRASES: Record<number, string> = {
|
|
18
|
+
400: "Bad Request",
|
|
19
|
+
401: "Unauthorized",
|
|
20
|
+
402: "Payment Required",
|
|
21
|
+
403: "Forbidden",
|
|
22
|
+
404: "Not Found",
|
|
23
|
+
405: "Method Not Allowed",
|
|
24
|
+
406: "Not Acceptable",
|
|
25
|
+
407: "Proxy Authentication Required",
|
|
26
|
+
408: "Request Timeout",
|
|
27
|
+
409: "Conflict",
|
|
28
|
+
410: "Gone",
|
|
29
|
+
411: "Length Required",
|
|
30
|
+
412: "Precondition Failed",
|
|
31
|
+
413: "Payload Too Large",
|
|
32
|
+
414: "URI Too Long",
|
|
33
|
+
415: "Unsupported Media Type",
|
|
34
|
+
416: "Range Not Satisfiable",
|
|
35
|
+
417: "Expectation Failed",
|
|
36
|
+
418: "I'm a Teapot",
|
|
37
|
+
421: "Misdirected Request",
|
|
38
|
+
422: "Unprocessable Entity",
|
|
39
|
+
423: "Locked",
|
|
40
|
+
424: "Failed Dependency",
|
|
41
|
+
425: "Too Early",
|
|
42
|
+
426: "Upgrade Required",
|
|
43
|
+
428: "Precondition Required",
|
|
44
|
+
429: "Too Many Requests",
|
|
45
|
+
431: "Request Header Fields Too Large",
|
|
46
|
+
451: "Unavailable For Legal Reasons",
|
|
47
|
+
500: "Internal Server Error",
|
|
48
|
+
501: "Not Implemented",
|
|
49
|
+
502: "Bad Gateway",
|
|
50
|
+
503: "Service Unavailable",
|
|
51
|
+
504: "Gateway Timeout",
|
|
52
|
+
505: "HTTP Version Not Supported",
|
|
53
|
+
506: "Variant Also Negotiates",
|
|
54
|
+
507: "Insufficient Storage",
|
|
55
|
+
508: "Loop Detected",
|
|
56
|
+
510: "Not Extended",
|
|
57
|
+
511: "Network Authentication Required",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function statusPhrase(status: number): string {
|
|
61
|
+
return STATUS_PHRASES[status] ?? "Error";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build an RFC 9457 problem+json body from a caught error.
|
|
66
|
+
* RouterError messages/codes are always exposed (developer-crafted).
|
|
14
67
|
* Standard Error messages are hidden in production.
|
|
68
|
+
*
|
|
69
|
+
* The `type` member is omitted in this phase: per RFC 9457 an absent `type` is
|
|
70
|
+
* treated as `"about:blank"` (no semantics beyond the HTTP status), so emitting
|
|
71
|
+
* it adds nothing. Per-route problem-type URIs arrive with the declared-errors
|
|
72
|
+
* map later. `code` is always present so consumers can branch on it
|
|
73
|
+
* (`"INTERNAL"` for non-RouterError failures).
|
|
15
74
|
*/
|
|
16
|
-
export function
|
|
75
|
+
export function createProblemDetails(
|
|
17
76
|
error: unknown,
|
|
77
|
+
status: number,
|
|
18
78
|
isDev: boolean,
|
|
19
|
-
):
|
|
79
|
+
): ProblemDetails {
|
|
20
80
|
if (error instanceof RouterError) {
|
|
21
81
|
return {
|
|
22
|
-
|
|
82
|
+
title: statusPhrase(status),
|
|
83
|
+
status,
|
|
84
|
+
detail: error.message,
|
|
23
85
|
code: error.code,
|
|
24
|
-
...(error.type ? { type: error.type } : {}),
|
|
25
86
|
...(isDev && error.stack ? { stack: error.stack } : {}),
|
|
26
87
|
};
|
|
27
88
|
}
|
|
28
89
|
if (error instanceof Error) {
|
|
29
90
|
return {
|
|
30
|
-
|
|
91
|
+
title: statusPhrase(status),
|
|
92
|
+
status,
|
|
93
|
+
detail: isDev ? error.message : "Internal Server Error",
|
|
94
|
+
code: "INTERNAL",
|
|
31
95
|
...(isDev && error.stack ? { stack: error.stack } : {}),
|
|
32
96
|
};
|
|
33
97
|
}
|
|
34
98
|
return {
|
|
35
|
-
|
|
99
|
+
title: statusPhrase(status),
|
|
100
|
+
status,
|
|
101
|
+
detail: isDev ? String(error) : "Internal Server Error",
|
|
102
|
+
code: "INTERNAL",
|
|
36
103
|
};
|
|
37
104
|
}
|
|
@@ -11,6 +11,7 @@ import { requireRequestContext } from "../server/request-context.js";
|
|
|
11
11
|
import { contextGet } from "../context-var.js";
|
|
12
12
|
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
13
13
|
import { traverseBack } from "../router/pattern-matching.js";
|
|
14
|
+
import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
|
|
14
15
|
import { createCacheScope } from "../cache/cache-scope.js";
|
|
15
16
|
import { executeMiddleware } from "../router/middleware.js";
|
|
16
17
|
import {
|
|
@@ -20,13 +21,15 @@ import {
|
|
|
20
21
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
21
22
|
import type { EntryData } from "../server/context.js";
|
|
22
23
|
import type { HandlerContext } from "./handler-context.js";
|
|
23
|
-
import {
|
|
24
|
+
import { createProblemDetails } from "./response-error.js";
|
|
24
25
|
import {
|
|
25
26
|
createResponseWithMergedHeaders,
|
|
26
27
|
finalizeResponse,
|
|
27
28
|
isCacheableStatus,
|
|
28
29
|
buildRouteMiddlewareEntries,
|
|
30
|
+
mergeStubHeadersAndFinalize,
|
|
29
31
|
} from "./helpers.js";
|
|
32
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
30
33
|
|
|
31
34
|
export interface ResponseRouteMatch {
|
|
32
35
|
responseType: string;
|
|
@@ -78,10 +81,13 @@ export async function handleResponseRoute<TEnv>(
|
|
|
78
81
|
env,
|
|
79
82
|
searchParams: cleanUrl.searchParams,
|
|
80
83
|
url: cleanUrl,
|
|
84
|
+
originalUrl: reqCtx.originalUrl,
|
|
81
85
|
pathname: url.pathname,
|
|
82
86
|
reverse: createReverseFunction(handlerCtx.getRequiredRouteMap()),
|
|
83
87
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
84
88
|
header: (name: string, value: string) => reqCtx.header(name, value),
|
|
89
|
+
waitUntil: reqCtx.waitUntil.bind(reqCtx),
|
|
90
|
+
executionContext: reqCtx.executionContext,
|
|
85
91
|
_responseType: preview.responseType,
|
|
86
92
|
};
|
|
87
93
|
// Brand with taint symbol so "use cache" detects it as request-scoped
|
|
@@ -96,6 +102,12 @@ export async function handleResponseRoute<TEnv>(
|
|
|
96
102
|
// so that stub headers (cookies, custom headers set via ctx.header()) are included.
|
|
97
103
|
// Use Headers (not Record<string, string>) to preserve duplicate entries like Set-Cookie.
|
|
98
104
|
const rewrapResponse = (result: Response) => {
|
|
105
|
+
// 204/205/304 are NOT short-circuited — they're valid for the Response
|
|
106
|
+
// constructor and must honor ctx.setStatus() overrides. Only upgrade
|
|
107
|
+
// responses (status 101 / `webSocket` property) bypass reconstruction.
|
|
108
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
109
|
+
return mergeStubHeadersAndFinalize(result);
|
|
110
|
+
}
|
|
99
111
|
const headers = new Headers();
|
|
100
112
|
result.headers.forEach((value, key) => {
|
|
101
113
|
if (key.toLowerCase() === "set-cookie") {
|
|
@@ -110,37 +122,6 @@ export async function handleResponseRoute<TEnv>(
|
|
|
110
122
|
});
|
|
111
123
|
};
|
|
112
124
|
|
|
113
|
-
// JSON response routes: wrap in { data } / { error } envelope
|
|
114
|
-
if (preview.responseType === "json") {
|
|
115
|
-
try {
|
|
116
|
-
const result = await (preview.handler as Function)(responseHandlerCtx);
|
|
117
|
-
if (result instanceof Response) {
|
|
118
|
-
return rewrapResponse(result);
|
|
119
|
-
}
|
|
120
|
-
return createResponseWithMergedHeaders(
|
|
121
|
-
JSON.stringify({ data: result }),
|
|
122
|
-
{
|
|
123
|
-
status: 200,
|
|
124
|
-
headers: { "content-type": "application/json;charset=utf-8" },
|
|
125
|
-
},
|
|
126
|
-
);
|
|
127
|
-
} catch (error) {
|
|
128
|
-
handlerCtx.callOnError(error, "handler", errorCtx);
|
|
129
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
130
|
-
const status = error instanceof RouterError ? error.status : 500;
|
|
131
|
-
return createResponseWithMergedHeaders(
|
|
132
|
-
JSON.stringify({
|
|
133
|
-
error: createResponseErrorPayload(error, isDev),
|
|
134
|
-
}),
|
|
135
|
-
{
|
|
136
|
-
status,
|
|
137
|
-
headers: { "content-type": "application/json;charset=utf-8" },
|
|
138
|
-
},
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Non-JSON response routes: catch errors and return plain Response
|
|
144
125
|
try {
|
|
145
126
|
const result = await (preview.handler as Function)(responseHandlerCtx);
|
|
146
127
|
|
|
@@ -148,38 +129,51 @@ export async function handleResponseRoute<TEnv>(
|
|
|
148
129
|
return rewrapResponse(result);
|
|
149
130
|
}
|
|
150
131
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
case "md":
|
|
169
|
-
return createResponseWithMergedHeaders(String(result), {
|
|
170
|
-
status: 200,
|
|
171
|
-
headers: { "content-type": "text/markdown;charset=utf-8" },
|
|
172
|
-
});
|
|
173
|
-
default:
|
|
174
|
-
// image, stream, any -- must return Response
|
|
175
|
-
throw new Error(
|
|
176
|
-
`Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
|
|
177
|
-
);
|
|
132
|
+
// Handled before the MIME lookup (json is also a RESPONSE_TYPE_MIME key).
|
|
133
|
+
if (preview.responseType === "json") {
|
|
134
|
+
return createResponseWithMergedHeaders(JSON.stringify(result), {
|
|
135
|
+
status: 200,
|
|
136
|
+
headers: { "content-type": "application/json;charset=utf-8" },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Object.hasOwn (not truthiness) so prototype names like "toString" are not
|
|
141
|
+
// matched; image/stream/any are absent and fall through to the throw.
|
|
142
|
+
if (Object.hasOwn(RESPONSE_TYPE_MIME, preview.responseType)) {
|
|
143
|
+
return createResponseWithMergedHeaders(String(result), {
|
|
144
|
+
status: 200,
|
|
145
|
+
headers: {
|
|
146
|
+
"content-type": `${RESPONSE_TYPE_MIME[preview.responseType]};charset=utf-8`,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
178
149
|
}
|
|
150
|
+
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
|
|
153
|
+
);
|
|
179
154
|
} catch (error) {
|
|
180
155
|
handlerCtx.callOnError(error, "handler", errorCtx);
|
|
181
156
|
const isDev = process.env.NODE_ENV !== "production";
|
|
182
|
-
const
|
|
157
|
+
const derivedStatus = error instanceof RouterError ? error.status : 500;
|
|
158
|
+
// Resolve the effective status the same way createResponseWithMergedHeaders
|
|
159
|
+
// will (ctx.res.status override) so the problem body's status/title match
|
|
160
|
+
// the actual HTTP status — e.g. when a handler called ctx.setStatus()
|
|
161
|
+
// before throwing.
|
|
162
|
+
const status =
|
|
163
|
+
reqCtx.res.status !== 200 ? reqCtx.res.status : derivedStatus;
|
|
164
|
+
|
|
165
|
+
if (preview.responseType === "json") {
|
|
166
|
+
return createResponseWithMergedHeaders(
|
|
167
|
+
JSON.stringify(createProblemDetails(error, status, isDev)),
|
|
168
|
+
{
|
|
169
|
+
status,
|
|
170
|
+
headers: {
|
|
171
|
+
"content-type": "application/problem+json;charset=utf-8",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
183
177
|
const message =
|
|
184
178
|
error instanceof RouterError
|
|
185
179
|
? error.message
|
|
@@ -196,7 +190,9 @@ export async function handleResponseRoute<TEnv>(
|
|
|
196
190
|
// Wrap callHandler to append Vary: Accept on content-negotiated responses
|
|
197
191
|
const callHandlerWithVary = async () => {
|
|
198
192
|
const response = await callHandler();
|
|
199
|
-
if (preview.negotiated) {
|
|
193
|
+
if (preview.negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
194
|
+
// Skip Vary on upgrade responses: headers are semantically immutable
|
|
195
|
+
// on some runtimes, and Vary is meaningless for a 101 response.
|
|
200
196
|
response.headers.append("Vary", "Accept");
|
|
201
197
|
}
|
|
202
198
|
return response;
|