@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.204030a9
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/AGENTS.md +4 -0
- package/README.md +242 -55
- package/dist/bin/rango.js +277 -99
- package/dist/vite/index.js +2929 -1132
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +68 -21
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +243 -21
- package/skills/caching/SKILL.md +159 -10
- 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 +262 -51
- package/skills/host-router/SKILL.md +243 -0
- 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 +249 -17
- package/skills/loader/SKILL.md +291 -31
- package/skills/middleware/SKILL.md +49 -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 +197 -6
- package/skills/prerender/SKILL.md +125 -102
- package/skills/rango/SKILL.md +242 -23
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +91 -8
- package/skills/router-setup/SKILL.md +98 -8
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +511 -188
- package/skills/typesafety/SKILL.md +354 -50
- 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 +117 -0
- package/src/__internal.ts +92 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +91 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +183 -18
- package/src/browser/navigation-client.ts +187 -57
- package/src/browser/navigation-store.ts +75 -17
- package/src/browser/navigation-transaction.ts +21 -37
- package/src/browser/partial-update.ts +143 -40
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +110 -33
- package/src/browser/react/context.ts +7 -2
- 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 +23 -64
- 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 +43 -10
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +200 -75
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +31 -36
- package/src/browser/types.ts +81 -5
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +69 -26
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -88
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +76 -49
- package/src/cache/cf/cf-cache-store.ts +501 -18
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +5 -1
- package/src/client.tsx +95 -284
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +65 -12
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +2 -5
- 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 -20
- package/src/index.rsc.ts +15 -40
- package/src/index.ts +92 -76
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +141 -80
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +435 -260
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +110 -34
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +113 -1
- package/src/router/error-handling.ts +1 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +105 -39
- package/src/router/intercept-resolution.ts +15 -22
- package/src/router/lazy-includes.ts +12 -9
- package/src/router/loader-resolution.ts +175 -23
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +129 -193
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +136 -106
- package/src/router/match-middleware/cache-store.ts +54 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +124 -18
- package/src/router/metrics.ts +239 -14
- package/src/router/middleware-types.ts +61 -31
- package/src/router/middleware.ts +226 -124
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +118 -19
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +286 -0
- package/src/router/revalidation.ts +85 -9
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +91 -29
- package/src/router/router-options.ts +89 -19
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +240 -23
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +483 -289
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +38 -15
- package/src/router/types.ts +9 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +120 -32
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +524 -370
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -21
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +39 -10
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +69 -89
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +39 -47
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/types.ts +19 -3
- package/src/search-params.ts +20 -17
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +219 -67
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +285 -63
- package/src/server/cookie-store.ts +28 -4
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +228 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +9 -1
- package/src/static-handler.ts +19 -7
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -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 +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +98 -0
- package/src/testing/internal/context.ts +151 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -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 +112 -0
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +197 -79
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +37 -6
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +50 -9
- package/src/urls/path-helper.ts +63 -63
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +487 -44
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +63 -91
- package/src/vite/discovery/discover-routers.ts +106 -53
- 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 +222 -107
- 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 +50 -13
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +10 -3
- package/src/vite/plugin-types.ts +111 -72
- 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 +55 -33
- package/src/vite/plugins/expose-id-utils.ts +24 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
- 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 +544 -317
- package/src/vite/plugins/performance-tracks.ts +92 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- 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 +72 -3
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +265 -226
- package/src/vite/router-discovery.ts +924 -137
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +4 -4
- 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 +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +98 -5
- package/src/vite/utils/shared-utils.ts +109 -27
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/route-definition/route-function.ts +0 -119
|
@@ -12,6 +12,9 @@ import type {
|
|
|
12
12
|
DefaultVars,
|
|
13
13
|
} from "../types/global-namespace.js";
|
|
14
14
|
import type { ScopedReverseFunction } from "../reverse.js";
|
|
15
|
+
import type { Theme } from "../theme/types.js";
|
|
16
|
+
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
17
|
+
import type { RequestScope } from "../types/request-scope.js";
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* Get variable function type
|
|
@@ -25,8 +28,12 @@ type GetVariableFn = {
|
|
|
25
28
|
* Set variable function type
|
|
26
29
|
*/
|
|
27
30
|
type SetVariableFn = {
|
|
28
|
-
<T>(contextVar: ContextVar<T>, value: T): void;
|
|
29
|
-
<K extends keyof DefaultVars>(
|
|
31
|
+
<T>(contextVar: ContextVar<T>, value: T, options?: { cache?: boolean }): void;
|
|
32
|
+
<K extends keyof DefaultVars>(
|
|
33
|
+
key: K,
|
|
34
|
+
value: DefaultVars[K],
|
|
35
|
+
options?: { cache?: boolean },
|
|
36
|
+
): void;
|
|
30
37
|
};
|
|
31
38
|
|
|
32
39
|
/**
|
|
@@ -46,38 +53,24 @@ export interface CookieOptions {
|
|
|
46
53
|
* Context passed to middleware
|
|
47
54
|
*
|
|
48
55
|
* @template TEnv - Environment type (bindings, variables) - defaults to any for internal flexibility
|
|
49
|
-
* @template TParams - URL params type (typed for route middleware,
|
|
56
|
+
* @template TParams - URL params type (typed for route middleware,
|
|
57
|
+
* `Record<string, string | undefined>` for global middleware — absent
|
|
58
|
+
* optional segments are omitted from the params record at runtime, so
|
|
59
|
+
* the index signature must include `undefined`)
|
|
50
60
|
*/
|
|
51
61
|
export interface MiddlewareContext<
|
|
52
62
|
TEnv = any,
|
|
53
|
-
TParams = Record<string, string>,
|
|
54
|
-
> {
|
|
55
|
-
/** Original request */
|
|
56
|
-
request: Request;
|
|
57
|
-
|
|
58
|
-
/** Parsed URL */
|
|
59
|
-
url: URL;
|
|
60
|
-
|
|
61
|
-
/** URL pathname */
|
|
62
|
-
pathname: string;
|
|
63
|
-
|
|
64
|
-
/** URL search params */
|
|
65
|
-
searchParams: URLSearchParams;
|
|
66
|
-
|
|
67
|
-
/** Platform bindings (Cloudflare, etc.) */
|
|
68
|
-
env: TEnv;
|
|
69
|
-
|
|
63
|
+
TParams = Record<string, string | undefined>,
|
|
64
|
+
> extends RequestScope<TEnv> {
|
|
70
65
|
/** URL params extracted from route/middleware pattern */
|
|
71
66
|
params: TParams;
|
|
72
67
|
|
|
73
68
|
/**
|
|
74
|
-
* Response
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* Use `ctx.header()` to set response headers, or `cookies()` for cookie mutations.
|
|
78
|
-
* To replace the response entirely, return a new `Response` from the middleware.
|
|
69
|
+
* Response headers.
|
|
70
|
+
* Before `next()`, returns headers from the shared response stub.
|
|
71
|
+
* After `next()`, returns headers from the downstream response.
|
|
79
72
|
*/
|
|
80
|
-
readonly
|
|
73
|
+
readonly headers: Headers;
|
|
81
74
|
|
|
82
75
|
/** Get a context variable (shared with route handlers) */
|
|
83
76
|
get: GetVariableFn;
|
|
@@ -86,11 +79,10 @@ export interface MiddlewareContext<
|
|
|
86
79
|
set: SetVariableFn;
|
|
87
80
|
|
|
88
81
|
/**
|
|
89
|
-
* Set a response header - can be called before or after `next()
|
|
82
|
+
* Set a response header - can be called before or after `next()`.
|
|
90
83
|
*
|
|
91
84
|
* When called before `next()`, headers are queued and merged into the final response.
|
|
92
85
|
* When called after `next()`, headers are set directly on the response.
|
|
93
|
-
* Shorthand for `ctx.res.headers.set()`.
|
|
94
86
|
*/
|
|
95
87
|
header(name: string, value: string): void;
|
|
96
88
|
|
|
@@ -100,6 +92,38 @@ export interface MiddlewareContext<
|
|
|
100
92
|
*/
|
|
101
93
|
routeName?: DefaultRouteName;
|
|
102
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Enable performance metrics for this request.
|
|
97
|
+
* When called, granular timing breakdown is logged to console and
|
|
98
|
+
* included in the Server-Timing response header, regardless of the
|
|
99
|
+
* router-level `debugPerformance` option.
|
|
100
|
+
*
|
|
101
|
+
* Call **before** `await next()` so the metrics store exists when
|
|
102
|
+
* downstream phases (route matching, rendering, SSR) record their
|
|
103
|
+
* spans. Calling after `next()` returns still emits `handler:total`
|
|
104
|
+
* but misses all upstream metrics.
|
|
105
|
+
*/
|
|
106
|
+
debugPerformance(): void;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Current theme (from cookie or default).
|
|
110
|
+
* Only available when theme is enabled in router config.
|
|
111
|
+
*/
|
|
112
|
+
theme?: Theme;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Set the theme (only available when theme is enabled in router config).
|
|
116
|
+
* Sets a cookie with the new theme value.
|
|
117
|
+
*/
|
|
118
|
+
setTheme?: (theme: Theme) => void;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Attach location state entries to this response.
|
|
122
|
+
* State is delivered to the client via history.pushState and accessible
|
|
123
|
+
* through the useLocationState() hook.
|
|
124
|
+
*/
|
|
125
|
+
setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
|
|
126
|
+
|
|
103
127
|
/**
|
|
104
128
|
* Generate URLs from route names.
|
|
105
129
|
* - `name` — global route, from the named-routes definition
|
|
@@ -116,7 +140,7 @@ export interface MiddlewareContext<
|
|
|
116
140
|
* @template TEnv - Environment type - defaults to any for internal flexibility
|
|
117
141
|
* @template TParams - URL params type (typed for route middleware)
|
|
118
142
|
*
|
|
119
|
-
* When using middleware with global augmentation (
|
|
143
|
+
* When using middleware with global augmentation (Rango.Env), explicitly
|
|
120
144
|
* annotate your middleware functions, or the types will be inferred from context:
|
|
121
145
|
*
|
|
122
146
|
* @example
|
|
@@ -128,7 +152,10 @@ export interface MiddlewareContext<
|
|
|
128
152
|
* router.use((ctx, next) => {...}) // ctx is typed from router's TEnv
|
|
129
153
|
* ```
|
|
130
154
|
*/
|
|
131
|
-
export type MiddlewareFn<
|
|
155
|
+
export type MiddlewareFn<
|
|
156
|
+
TEnv = any,
|
|
157
|
+
TParams = Record<string, string | undefined>,
|
|
158
|
+
> = (
|
|
132
159
|
ctx: MiddlewareContext<TEnv, TParams>,
|
|
133
160
|
next: () => Promise<Response>,
|
|
134
161
|
) => Response | void | Promise<Response | void>;
|
|
@@ -155,7 +182,7 @@ export interface MiddlewareEntry<TEnv = any> {
|
|
|
155
182
|
}
|
|
156
183
|
|
|
157
184
|
/**
|
|
158
|
-
* Mutable response holder -
|
|
185
|
+
* Mutable response holder - tracks the current response through the middleware chain.
|
|
159
186
|
*/
|
|
160
187
|
export interface ResponseHolder {
|
|
161
188
|
response: Response | null;
|
|
@@ -175,5 +202,8 @@ export interface MiddlewareCollectableEntry {
|
|
|
175
202
|
*/
|
|
176
203
|
export interface CollectedMiddleware {
|
|
177
204
|
handler: MiddlewareFn<any, any>;
|
|
205
|
+
// Internal shape only. The user-facing `MiddlewareContext.params` is
|
|
206
|
+
// typed `Record<string, string | undefined>` to reflect that absent
|
|
207
|
+
// optional segments are omitted from the params record at runtime.
|
|
178
208
|
params: Record<string, string>;
|
|
179
209
|
}
|
package/src/router/middleware.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { contextGet, contextSet } from "../context-var.js";
|
|
13
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
14
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
13
15
|
import type {
|
|
14
16
|
CollectedMiddleware,
|
|
15
17
|
MiddlewareCollectableEntry,
|
|
@@ -20,6 +22,9 @@ import type {
|
|
|
20
22
|
} from "./middleware-types.js";
|
|
21
23
|
import { _getRequestContext } from "../server/request-context.js";
|
|
22
24
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
25
|
+
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
26
|
+
import { stripInternalParams } from "./handler-context.js";
|
|
27
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
23
28
|
|
|
24
29
|
// Re-export types and cookie utilities for backward compatibility
|
|
25
30
|
export type {
|
|
@@ -33,25 +38,29 @@ export type {
|
|
|
33
38
|
} from "./middleware-types.js";
|
|
34
39
|
export { parseCookies, serializeCookie } from "./middleware-cookies.js";
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
const MIDDLEWARE_METRIC_DEPTH = 1;
|
|
42
|
+
/** Ignore post-next() durations below this threshold (measurement noise). */
|
|
43
|
+
const POST_METRIC_MIN_DURATION_MS = 0.01;
|
|
44
|
+
|
|
45
|
+
function getMiddlewareMetricBase<TEnv>(
|
|
46
|
+
entry: MiddlewareEntry<TEnv>,
|
|
47
|
+
ordinal: number,
|
|
48
|
+
): string {
|
|
49
|
+
const handlerName = entry.handler.name?.trim();
|
|
50
|
+
const scope = entry.pattern ?? "*";
|
|
51
|
+
|
|
52
|
+
if (handlerName) {
|
|
53
|
+
return `${handlerName}@${scope}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return `${scope}#${ordinal + 1}`;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
function getMiddlewareMetricLabel<TEnv>(
|
|
60
|
+
entry: MiddlewareEntry<TEnv>,
|
|
61
|
+
ordinal: number,
|
|
62
|
+
): string {
|
|
63
|
+
return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
/**
|
|
@@ -106,7 +115,12 @@ function escapeRegex(str: string): string {
|
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
/**
|
|
109
|
-
* Extract params from a pathname using a pattern's regex and param names
|
|
118
|
+
* Extract params from a pathname using a pattern's regex and param names.
|
|
119
|
+
*
|
|
120
|
+
* Values are URL-decoded so apps see the raw string (e.g. "ivo@example.com")
|
|
121
|
+
* instead of the percent-encoded form ("ivo%40example.com"). This matches the
|
|
122
|
+
* contract assumed by ctx.reverse (which re-encodes) and aligns with
|
|
123
|
+
* Express/React Router/Fastify/Koa.
|
|
110
124
|
*/
|
|
111
125
|
export function extractParams(
|
|
112
126
|
pathname: string,
|
|
@@ -118,7 +132,7 @@ export function extractParams(
|
|
|
118
132
|
|
|
119
133
|
const params: Record<string, string> = {};
|
|
120
134
|
for (let i = 0; i < paramNames.length; i++) {
|
|
121
|
-
params[paramNames[i]] = match[i + 1] || "";
|
|
135
|
+
params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
|
|
122
136
|
}
|
|
123
137
|
return params;
|
|
124
138
|
}
|
|
@@ -142,7 +156,7 @@ export function createMiddlewareContext<TEnv>(
|
|
|
142
156
|
search?: Record<string, unknown>,
|
|
143
157
|
) => string,
|
|
144
158
|
): MiddlewareContext<TEnv> {
|
|
145
|
-
const url = new URL(request.url);
|
|
159
|
+
const url = stripInternalParams(new URL(request.url));
|
|
146
160
|
|
|
147
161
|
// Track the initial response to detect pre/post-next() phase.
|
|
148
162
|
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
@@ -158,13 +172,37 @@ export function createMiddlewareContext<TEnv>(
|
|
|
158
172
|
// Cookie operations are handled by the standalone cookies() function which
|
|
159
173
|
// delegates to the shared RequestContext internally.
|
|
160
174
|
// The runtime implementation - types are enforced at call sites via MiddlewareContext<TEnv>
|
|
175
|
+
// Internal helper: resolve the current response (stub before next(), real after).
|
|
176
|
+
// Not exposed on the public MiddlewareContext type — use ctx.headers instead.
|
|
177
|
+
const getResponse = (): Response => {
|
|
178
|
+
if (isPreNext()) {
|
|
179
|
+
const reqCtx = _getRequestContext();
|
|
180
|
+
if (reqCtx) return reqCtx.res;
|
|
181
|
+
}
|
|
182
|
+
if (!responseHolder.response) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
"Response is not available - responseHolder was not initialized",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return responseHolder.response;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Capture reqCtx once: the request-scoped platform fields
|
|
191
|
+
// (originalUrl, executionContext, waitUntil) are immutable per request,
|
|
192
|
+
// so snapshotting beats re-reading ALS on every access. The lazy getters
|
|
193
|
+
// below (routeName, theme, setTheme) stay lazy because those can change
|
|
194
|
+
// during `await next()`.
|
|
195
|
+
const reqCtx = _getRequestContext();
|
|
161
196
|
return {
|
|
162
197
|
request,
|
|
163
198
|
url,
|
|
199
|
+
originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
|
|
164
200
|
pathname: url.pathname,
|
|
165
201
|
searchParams: url.searchParams,
|
|
166
202
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
167
203
|
params,
|
|
204
|
+
executionContext: reqCtx?.executionContext,
|
|
205
|
+
waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
|
|
168
206
|
// Getter: re-derives from request context on each access so that global
|
|
169
207
|
// middleware sees the matched route name after await next().
|
|
170
208
|
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
@@ -175,33 +213,16 @@ export function createMiddlewareContext<TEnv>(
|
|
|
175
213
|
) as MiddlewareContext<TEnv>["routeName"];
|
|
176
214
|
},
|
|
177
215
|
|
|
178
|
-
get
|
|
179
|
-
|
|
180
|
-
// set via ctx.header() are visible on ctx.res.
|
|
181
|
-
if (isPreNext()) {
|
|
182
|
-
const reqCtx = _getRequestContext();
|
|
183
|
-
if (reqCtx) return reqCtx.res;
|
|
184
|
-
}
|
|
185
|
-
if (!responseHolder.response) {
|
|
186
|
-
throw new Error(
|
|
187
|
-
"ctx.res is not available - responseHolder was not initialized",
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
return responseHolder.response;
|
|
191
|
-
},
|
|
192
|
-
set res(_: Response) {
|
|
193
|
-
throw new Error(
|
|
194
|
-
"ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
|
|
195
|
-
);
|
|
216
|
+
get headers(): Headers {
|
|
217
|
+
return getResponse().headers;
|
|
196
218
|
},
|
|
197
219
|
|
|
198
220
|
get: ((keyOrVar: any) =>
|
|
199
221
|
contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
|
|
200
222
|
|
|
201
|
-
set: ((keyOrVar: any, value: unknown) => {
|
|
202
|
-
contextSet(variables, keyOrVar, value);
|
|
223
|
+
set: ((keyOrVar: any, value: unknown, options?: any) => {
|
|
224
|
+
contextSet(variables, keyOrVar, value, options);
|
|
203
225
|
}) as MiddlewareContext<TEnv>["set"],
|
|
204
|
-
|
|
205
226
|
header(name: string, value: string): void {
|
|
206
227
|
// Before next(): delegate to shared RequestContext stub
|
|
207
228
|
if (isPreNext()) {
|
|
@@ -220,6 +241,24 @@ export function createMiddlewareContext<TEnv>(
|
|
|
220
241
|
responseHolder.response.headers.set(name, value);
|
|
221
242
|
},
|
|
222
243
|
|
|
244
|
+
get theme(): MiddlewareContext<TEnv>["theme"] {
|
|
245
|
+
return _getRequestContext()?.theme;
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
get setTheme(): MiddlewareContext<TEnv>["setTheme"] {
|
|
249
|
+
return _getRequestContext()?.setTheme;
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
setLocationState(entries) {
|
|
253
|
+
const reqCtx = _getRequestContext();
|
|
254
|
+
if (!reqCtx) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
"setLocationState() is not available outside a request context",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
reqCtx.setLocationState(entries);
|
|
260
|
+
},
|
|
261
|
+
|
|
223
262
|
reverse:
|
|
224
263
|
reverse ??
|
|
225
264
|
((name: string) => {
|
|
@@ -227,6 +266,14 @@ export function createMiddlewareContext<TEnv>(
|
|
|
227
266
|
`ctx.reverse() is not available - route map was not provided to middleware context`,
|
|
228
267
|
);
|
|
229
268
|
}),
|
|
269
|
+
|
|
270
|
+
debugPerformance(): void {
|
|
271
|
+
const reqCtx = _getRequestContext();
|
|
272
|
+
if (reqCtx) {
|
|
273
|
+
reqCtx._debugPerformance = true;
|
|
274
|
+
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
230
277
|
};
|
|
231
278
|
}
|
|
232
279
|
|
|
@@ -260,14 +307,54 @@ export function matchMiddleware<TEnv>(
|
|
|
260
307
|
return matches;
|
|
261
308
|
}
|
|
262
309
|
|
|
310
|
+
// Set-Cookie is appended; for other headers stubOverridesNonCookie=true
|
|
311
|
+
// overwrites (chain ran to completion), false fills only missing slots (an
|
|
312
|
+
// explicit short-circuit Response's own headers win).
|
|
313
|
+
function mergeStubHeaders(
|
|
314
|
+
target: Headers,
|
|
315
|
+
stub: Headers,
|
|
316
|
+
stubOverridesNonCookie: boolean,
|
|
317
|
+
): void {
|
|
318
|
+
stub.forEach((value, name) => {
|
|
319
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
320
|
+
target.append(name, value);
|
|
321
|
+
} else if (stubOverridesNonCookie || !target.has(name)) {
|
|
322
|
+
target.set(name, value);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Set-Cookie is deduped so a nested inner executeMiddleware that already merged
|
|
328
|
+
// the same reqCtx cookies does not duplicate them; other headers fill if missing.
|
|
329
|
+
function mergeReqCtxStub(
|
|
330
|
+
target: Headers,
|
|
331
|
+
reqCtx: ReturnType<typeof _getRequestContext>,
|
|
332
|
+
): void {
|
|
333
|
+
if (!reqCtx) return;
|
|
334
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
335
|
+
if (stubCookies.length > 0) {
|
|
336
|
+
const existing = new Set(target.getSetCookie());
|
|
337
|
+
for (const cookie of stubCookies) {
|
|
338
|
+
if (!existing.has(cookie)) {
|
|
339
|
+
target.append("set-cookie", cookie);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
344
|
+
if (name !== "set-cookie" && !target.has(name)) {
|
|
345
|
+
target.set(name, value);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
263
350
|
/**
|
|
264
351
|
* Execute middleware chain
|
|
265
352
|
*
|
|
266
353
|
* Features:
|
|
267
354
|
* - `await next()` returns actual Response
|
|
268
|
-
* - `ctx.
|
|
269
|
-
* - `ctx.header()` shorthand for setting
|
|
270
|
-
* - Forgiving: if middleware doesn't return, uses
|
|
355
|
+
* - `ctx.headers` available before and after `await next()`
|
|
356
|
+
* - `ctx.header()` shorthand for setting a single header
|
|
357
|
+
* - Forgiving: if middleware doesn't return, uses the downstream response
|
|
271
358
|
* - Short-circuit: return Response to stop chain
|
|
272
359
|
* - Error catching: try/catch around `next()` works
|
|
273
360
|
*/
|
|
@@ -298,28 +385,13 @@ export async function executeMiddleware<TEnv>(
|
|
|
298
385
|
// End of chain - call actual RSC handler
|
|
299
386
|
const response = await finalHandler();
|
|
300
387
|
|
|
301
|
-
// Merge headers set on stub into the real response.
|
|
302
|
-
// Use append for Set-Cookie to preserve multiple cookies.
|
|
303
388
|
const mergedHeaders = new Headers(response.headers);
|
|
304
|
-
stubResponse.headers
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
});
|
|
311
|
-
// Also merge shared RequestContext stub (cookies written via cookies().set()).
|
|
312
|
-
// Set-Cookie duplication is prevented by createResponseWithMergedHeaders
|
|
313
|
-
// draining Set-Cookie from ctx.res after merging (helpers.ts).
|
|
314
|
-
const reqCtx = _getRequestContext();
|
|
315
|
-
if (reqCtx) {
|
|
316
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
317
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
318
|
-
mergedHeaders.append(name, value);
|
|
319
|
-
} else if (!mergedHeaders.has(name)) {
|
|
320
|
-
mergedHeaders.set(name, value);
|
|
321
|
-
}
|
|
322
|
-
});
|
|
389
|
+
mergeStubHeaders(mergedHeaders, stubResponse.headers, true);
|
|
390
|
+
mergeReqCtxStub(mergedHeaders, _getRequestContext());
|
|
391
|
+
|
|
392
|
+
if (isWebSocketUpgradeResponse(response)) {
|
|
393
|
+
responseHolder.response = response;
|
|
394
|
+
return response;
|
|
323
395
|
}
|
|
324
396
|
|
|
325
397
|
// Clone response with merged headers (mutable for post-next() modifications)
|
|
@@ -332,6 +404,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
332
404
|
return responseHolder.response;
|
|
333
405
|
}
|
|
334
406
|
|
|
407
|
+
const middlewareOrdinal = index;
|
|
335
408
|
const { entry, params } = middlewares[index++];
|
|
336
409
|
const ctx = createMiddlewareContext(
|
|
337
410
|
request,
|
|
@@ -341,67 +414,92 @@ export async function executeMiddleware<TEnv>(
|
|
|
341
414
|
responseHolder,
|
|
342
415
|
reverse,
|
|
343
416
|
);
|
|
417
|
+
const metricStart = performance.now();
|
|
418
|
+
const metricLabel = getMiddlewareMetricLabel(entry, middlewareOrdinal);
|
|
419
|
+
let middlewareFinished = false;
|
|
420
|
+
const finishMiddleware = () => {
|
|
421
|
+
if (!middlewareFinished) {
|
|
422
|
+
middlewareFinished = true;
|
|
423
|
+
appendMetric(
|
|
424
|
+
_getRequestContext()?._metricsStore,
|
|
425
|
+
`${metricLabel}:pre`,
|
|
426
|
+
metricStart,
|
|
427
|
+
performance.now() - metricStart,
|
|
428
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
344
432
|
|
|
345
433
|
// Track if next() was called and capture its Promise.
|
|
346
434
|
// Guard against double-calling: a second call would re-enter the
|
|
347
435
|
// downstream chain and overwrite responseHolder.response.
|
|
348
436
|
let nextPromise: Promise<Response> | null = null;
|
|
437
|
+
let nextResolvedAt: number | undefined;
|
|
349
438
|
const wrappedNext = (): Promise<Response> => {
|
|
350
439
|
if (nextPromise) {
|
|
351
440
|
throw new Error(
|
|
352
441
|
`[@rangojs/router] Middleware called next() more than once.`,
|
|
353
442
|
);
|
|
354
443
|
}
|
|
355
|
-
|
|
444
|
+
finishMiddleware();
|
|
445
|
+
const downstream = next();
|
|
446
|
+
nextPromise = downstream.then(
|
|
447
|
+
(res) => {
|
|
448
|
+
nextResolvedAt = performance.now();
|
|
449
|
+
return res;
|
|
450
|
+
},
|
|
451
|
+
(err) => {
|
|
452
|
+
nextResolvedAt = performance.now();
|
|
453
|
+
throw err;
|
|
454
|
+
},
|
|
455
|
+
);
|
|
356
456
|
return nextPromise;
|
|
357
457
|
};
|
|
358
458
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
459
|
+
let result: Response | void;
|
|
460
|
+
try {
|
|
461
|
+
result = await entry.handler(ctx, wrappedNext);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
// Thrown Response is short-circuit control flow, not an error.
|
|
464
|
+
// Fall through to the `if (result instanceof Response)` branch below
|
|
465
|
+
// so stub headers and request-context cookies merge as they do for
|
|
466
|
+
// an explicit `return new Response(...)`. Real errors propagate.
|
|
467
|
+
if (error instanceof Response) {
|
|
468
|
+
result = error;
|
|
469
|
+
} else {
|
|
470
|
+
finishMiddleware();
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
finishMiddleware();
|
|
475
|
+
|
|
476
|
+
// Record post-next() processing time when middleware did work after
|
|
477
|
+
// the downstream chain resolved (e.g. adding headers, logging).
|
|
478
|
+
if (nextResolvedAt !== undefined) {
|
|
479
|
+
const postDur = performance.now() - nextResolvedAt;
|
|
480
|
+
if (postDur > POST_METRIC_MIN_DURATION_MS) {
|
|
481
|
+
appendMetric(
|
|
482
|
+
_getRequestContext()?._metricsStore,
|
|
483
|
+
`${metricLabel}:post`,
|
|
484
|
+
nextResolvedAt,
|
|
485
|
+
postDur,
|
|
486
|
+
MIDDLEWARE_METRIC_DEPTH,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
367
489
|
}
|
|
368
|
-
|
|
369
|
-
const result = await entry.handler(ctx, wrappedNext);
|
|
370
490
|
|
|
371
491
|
// Explicit return takes precedence (middleware short-circuit).
|
|
372
492
|
// Merge stub headers (from ctx.header before this point) and
|
|
373
493
|
// RequestContext stub headers (from ctx.setCookie) into the
|
|
374
494
|
// returned Response so they are not lost.
|
|
375
495
|
if (result instanceof Response) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
ctxSetCalled &&
|
|
380
|
-
result.status >= 300 &&
|
|
381
|
-
result.status < 400
|
|
382
|
-
) {
|
|
383
|
-
warnCtxSetBeforeRedirect(entry.handler);
|
|
496
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
497
|
+
responseHolder.response = result;
|
|
498
|
+
return result;
|
|
384
499
|
}
|
|
385
|
-
|
|
386
500
|
const mergedHeaders = new Headers(result.headers);
|
|
387
|
-
stubResponse.headers
|
|
388
|
-
|
|
389
|
-
mergedHeaders.append(name, value);
|
|
390
|
-
} else if (!mergedHeaders.has(name)) {
|
|
391
|
-
mergedHeaders.set(name, value);
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
// Also merge shared RequestContext stub (cookies written via setCookie)
|
|
395
|
-
const reqCtx = _getRequestContext();
|
|
396
|
-
if (reqCtx) {
|
|
397
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
398
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
399
|
-
mergedHeaders.append(name, value);
|
|
400
|
-
} else if (!mergedHeaders.has(name)) {
|
|
401
|
-
mergedHeaders.set(name, value);
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
}
|
|
501
|
+
mergeStubHeaders(mergedHeaders, stubResponse.headers, false);
|
|
502
|
+
mergeReqCtxStub(mergedHeaders, _getRequestContext());
|
|
405
503
|
const merged = new Response(result.body, {
|
|
406
504
|
status: result.status,
|
|
407
505
|
statusText: result.statusText,
|
|
@@ -424,19 +522,6 @@ export async function executeMiddleware<TEnv>(
|
|
|
424
522
|
// If middleware called next(), await it and return the response
|
|
425
523
|
if (nextPromise) {
|
|
426
524
|
await nextPromise;
|
|
427
|
-
|
|
428
|
-
// W5: warn if ctx.set() was called but the downstream response is a redirect.
|
|
429
|
-
// The ctx.set() values will be lost because the redirect navigates away.
|
|
430
|
-
if (
|
|
431
|
-
process.env.NODE_ENV !== "production" &&
|
|
432
|
-
ctxSetCalled &&
|
|
433
|
-
responseHolder.response &&
|
|
434
|
-
responseHolder.response.status >= 300 &&
|
|
435
|
-
responseHolder.response.status < 400
|
|
436
|
-
) {
|
|
437
|
-
warnCtxSetBeforeRedirect(entry.handler);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
525
|
return responseHolder.response!;
|
|
441
526
|
}
|
|
442
527
|
|
|
@@ -459,6 +544,18 @@ export async function executeMiddleware<TEnv>(
|
|
|
459
544
|
throw new Error("No response generated by middleware chain");
|
|
460
545
|
}
|
|
461
546
|
|
|
547
|
+
// Final re-merge: capture any RequestContext stub headers added after the
|
|
548
|
+
// last merge point (e.g. cookies().set() called after await next()).
|
|
549
|
+
// The reqCtx stub may have already been partially merged during finalHandler
|
|
550
|
+
// or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
|
|
551
|
+
//
|
|
552
|
+
// Skip for upgrade responses: upgrade headers are semantically immutable and
|
|
553
|
+
// set-cookie on an upgrade is not meaningful.
|
|
554
|
+
const reqCtx = _getRequestContext();
|
|
555
|
+
if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
|
|
556
|
+
mergeReqCtxStub(finalResponse.headers, reqCtx);
|
|
557
|
+
}
|
|
558
|
+
|
|
462
559
|
return finalResponse;
|
|
463
560
|
}
|
|
464
561
|
|
|
@@ -526,7 +623,18 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
526
623
|
return next();
|
|
527
624
|
};
|
|
528
625
|
|
|
529
|
-
|
|
626
|
+
let result: Response | void;
|
|
627
|
+
try {
|
|
628
|
+
result = await middleware(ctx, guardedNext);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
// Thrown Response is short-circuit control flow, parity with the
|
|
631
|
+
// explicit-return path below. Real errors propagate.
|
|
632
|
+
if (error instanceof Response) {
|
|
633
|
+
result = error;
|
|
634
|
+
} else {
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
530
638
|
|
|
531
639
|
if (result instanceof Response) {
|
|
532
640
|
earlyResponse = result;
|
|
@@ -554,13 +662,7 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
554
662
|
// Only fill in missing headers — the returned Response's explicit
|
|
555
663
|
// headers take precedence, matching executeMiddleware behavior.
|
|
556
664
|
const mergedHeaders = new Headers(response.headers);
|
|
557
|
-
stubResponse.headers
|
|
558
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
559
|
-
mergedHeaders.append(name, value);
|
|
560
|
-
} else if (!mergedHeaders.has(name)) {
|
|
561
|
-
mergedHeaders.set(name, value);
|
|
562
|
-
}
|
|
563
|
-
});
|
|
665
|
+
mergeStubHeaders(mergedHeaders, stubResponse.headers, false);
|
|
564
666
|
return new Response(response.body, {
|
|
565
667
|
status: response.status,
|
|
566
668
|
statusText: response.statusText,
|