@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7c7e4327
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 +8 -4
- package/README.md +301 -797
- package/dist/bin/rango.js +603 -145
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3750 -1160
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +96 -24
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +85 -6
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +228 -33
- package/skills/caching/SKILL.md +336 -19
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +110 -4
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +87 -56
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +12 -10
- package/skills/hooks/SKILL.md +73 -691
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +129 -27
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +75 -19
- package/skills/layout/SKILL.md +40 -19
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +248 -10
- package/skills/middleware/SKILL.md +25 -13
- package/skills/migrate-nextjs/SKILL.md +205 -20
- package/skills/migrate-react-router/SKILL.md +59 -670
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +29 -2
- package/skills/observability/SKILL.md +202 -0
- package/skills/parallel/SKILL.md +40 -10
- package/skills/ppr/SKILL.md +616 -0
- package/skills/prerender/SKILL.md +72 -60
- package/skills/rango/SKILL.md +318 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +138 -49
- package/skills/route/SKILL.md +117 -9
- package/skills/router-setup/SKILL.md +44 -9
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +776 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -626
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +74 -15
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +252 -158
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +94 -25
- package/src/browser/navigation-client.ts +144 -79
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +161 -73
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +183 -144
- package/src/browser/prefetch/cache.ts +242 -77
- package/src/browser/prefetch/fetch.ts +325 -69
- package/src/browser/prefetch/queue.ts +61 -12
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +58 -20
- package/src/browser/react/NavigationProvider.tsx +202 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +145 -28
- package/src/browser/scroll-restoration.ts +37 -22
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +236 -65
- package/src/browser/types.ts +102 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +203 -154
- package/src/build/generate-route-types.ts +3 -1
- package/src/build/index.ts +11 -3
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -21
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +456 -62
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +128 -51
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +13 -21
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +421 -58
- package/src/cache/cache-scope.ts +187 -96
- package/src/cache/cache-tag.ts +149 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2202 -372
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +180 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1127 -0
- package/src/client.rsc.tsx +41 -21
- package/src/client.tsx +33 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +67 -37
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +9 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +97 -12
- package/src/index.ts +98 -14
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +78 -10
- package/src/redirect-origin.ts +114 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -39
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +87 -59
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +27 -42
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +82 -59
- package/src/router/loader-resolution.ts +167 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +80 -55
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +211 -165
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +69 -79
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +298 -172
- package/src/router/navigation-snapshot.ts +7 -56
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +181 -150
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +112 -67
- package/src/router/preview-match.ts +6 -2
- package/src/router/request-classification.ts +50 -69
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +115 -36
- package/src/router/router-options.ts +166 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +131 -86
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +139 -39
- package/src/router/segment-resolution/loader-mask.ts +67 -0
- package/src/router/segment-resolution/loader-snapshot.ts +251 -0
- package/src/router/segment-resolution/revalidation.ts +272 -320
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +75 -0
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +105 -20
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -59
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +157 -71
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +291 -217
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +29 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +124 -13
- package/src/rsc/redirect-guard.ts +100 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +421 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +282 -116
- package/src/rsc/shell-capture.ts +1158 -0
- package/src/rsc/shell-serve.ts +150 -0
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +53 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +350 -149
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +208 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +152 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +395 -176
- package/src/ssr/index.tsx +458 -178
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -0
- package/src/testing/dispatch.ts +813 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +199 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +584 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -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 +305 -0
- package/src/theme/ThemeProvider.tsx +56 -84
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +19 -64
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -11
- package/src/urls/path-helper-types.ts +63 -17
- package/src/urls/path-helper.ts +22 -106
- package/src/urls/pattern-types.ts +72 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +98 -154
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +127 -86
- package/src/vite/discovery/discovery-errors.ts +255 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +96 -68
- 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 +45 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +210 -10
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- 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 +194 -0
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +138 -27
- package/src/vite/rango.ts +236 -138
- package/src/vite/router-discovery.ts +927 -136
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +71 -43
- package/src/vite/utils/shared-utils.ts +142 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
package/src/router/middleware.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/// <reference types="vite/types/importMeta.d.ts" />
|
|
2
|
-
/**
|
|
3
|
-
* Middleware Execution
|
|
4
|
-
*
|
|
5
|
-
* True middleware that wraps the entire RSC handler.
|
|
6
|
-
* - `await next()` returns actual Response
|
|
7
|
-
* - Can modify response headers
|
|
8
|
-
* - Can catch errors from RSC rendering
|
|
9
|
-
* - Forgiving API: if middleware doesn't return, original response is used
|
|
10
|
-
*/
|
|
11
2
|
|
|
12
3
|
import { contextGet, contextSet } from "../context-var.js";
|
|
4
|
+
import { escapeRegExp } from "../regex-escape.js";
|
|
5
|
+
import { parsePattern as parseRoutePattern } from "./pattern-matching.js";
|
|
6
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
7
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
13
8
|
import type {
|
|
14
9
|
CollectedMiddleware,
|
|
15
10
|
MiddlewareCollectableEntry,
|
|
@@ -19,27 +14,29 @@ import type {
|
|
|
19
14
|
ResponseHolder,
|
|
20
15
|
} from "./middleware-types.js";
|
|
21
16
|
import { _getRequestContext } from "../server/request-context.js";
|
|
17
|
+
import {
|
|
18
|
+
EXTERNAL_REDIRECT_MARKER,
|
|
19
|
+
isExternalRedirect,
|
|
20
|
+
markExternalRedirect,
|
|
21
|
+
} from "../redirect-origin.js";
|
|
22
22
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
23
23
|
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
24
|
+
import { observePhase, PHASES } from "./instrument.js";
|
|
24
25
|
import { stripInternalParams } from "./handler-context.js";
|
|
26
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
25
27
|
|
|
26
|
-
// Re-export types
|
|
28
|
+
// Re-export types consumed through this module's path.
|
|
27
29
|
export type {
|
|
28
30
|
CookieOptions,
|
|
29
|
-
CollectedMiddleware,
|
|
30
|
-
MiddlewareCollectableEntry,
|
|
31
31
|
MiddlewareContext,
|
|
32
32
|
MiddlewareEntry,
|
|
33
33
|
MiddlewareFn,
|
|
34
|
-
ResponseHolder,
|
|
35
34
|
} from "./middleware-types.js";
|
|
36
|
-
export { parseCookies, serializeCookie } from "./middleware-cookies.js";
|
|
37
35
|
|
|
38
36
|
const MIDDLEWARE_METRIC_DEPTH = 1;
|
|
39
|
-
/** Ignore post-next() durations below this threshold (measurement noise). */
|
|
40
37
|
const POST_METRIC_MIN_DURATION_MS = 0.01;
|
|
41
38
|
|
|
42
|
-
function
|
|
39
|
+
function getMiddlewareMetricLabel<TEnv>(
|
|
43
40
|
entry: MiddlewareEntry<TEnv>,
|
|
44
41
|
ordinal: number,
|
|
45
42
|
): string {
|
|
@@ -47,24 +44,49 @@ function getMiddlewareMetricBase<TEnv>(
|
|
|
47
44
|
const scope = entry.pattern ?? "*";
|
|
48
45
|
|
|
49
46
|
if (handlerName) {
|
|
50
|
-
return
|
|
47
|
+
return `middleware:${handlerName}@${scope}`;
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function getMiddlewareMetricLabel<TEnv>(
|
|
57
|
-
entry: MiddlewareEntry<TEnv>,
|
|
58
|
-
ordinal: number,
|
|
59
|
-
): string {
|
|
60
|
-
return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
|
|
50
|
+
return `middleware:${scope}#${ordinal + 1}`;
|
|
61
51
|
}
|
|
62
52
|
|
|
63
53
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
54
|
+
* Compile a middleware scope pattern to a regex + param names.
|
|
55
|
+
*
|
|
56
|
+
* Middleware scopes reuse the route pattern parser (`parsePattern` from
|
|
57
|
+
* pattern-matching.ts), so they support the same param forms as routes —
|
|
58
|
+
* optional (`:x?`), constrained (`:x(en|gb)`), and suffix (`:x.html`) — in
|
|
59
|
+
* addition to the trailing-`*` wildcard middleware relies on. Before this
|
|
60
|
+
* unification the middleware-side parser handled only static, bare `:param`,
|
|
61
|
+
* and trailing `*`, so e.g. `router.use("/:locale(en|gb)/*", mw)` silently
|
|
62
|
+
* named the param "locale(en|gb)" and never enforced the constraint.
|
|
63
|
+
*
|
|
64
|
+
* Middleware matching semantics deliberately differ from route matching, so we
|
|
65
|
+
* emit the regex here rather than route through `compilePattern`:
|
|
66
|
+
* - `*` alone matches every path (`/^.*$/`).
|
|
67
|
+
* - A trailing `*` segment is an OPTIONAL subtree match (`(?:/.*)?`): `/admin/*`
|
|
68
|
+
* matches `/admin`, `/admin/`, and `/admin/users`. It contributes no param
|
|
69
|
+
* name (unlike route wildcards, which capture `*`).
|
|
70
|
+
* - A NON-trailing `*` is also OPTIONAL (`(?:/.*)?`), matching zero-or-more
|
|
71
|
+
* intermediate segments: `/a/<star>/b` matches both `/a/b` and `/a/x/b`. This
|
|
72
|
+
* mirrors the pre-unification parser, which compiled every `*` part as
|
|
73
|
+
* optional regardless of position.
|
|
74
|
+
* - A pattern without a trailing `*` tolerates a trailing slash (`/?$`).
|
|
75
|
+
* - Constraints are baked into the regex as an alternation so `matchMiddleware`
|
|
76
|
+
* (a bare `regex.test`) enforces them without extra validation. Constraint
|
|
77
|
+
* values are matched against the raw (still URL-encoded) path segment, which
|
|
78
|
+
* matches the pre-unification middleware behavior (it never decoded for
|
|
79
|
+
* matching); the constraint string is regex-escaped so values like `en.gb`
|
|
80
|
+
* are treated literally.
|
|
81
|
+
*
|
|
82
|
+
* The route segment parser only recognizes `/`-prefixed segments, but the
|
|
83
|
+
* pre-unification middleware parser split on `/` and dropped empty parts, so a
|
|
84
|
+
* leading slash was irrelevant: `use("admin/*")` and `use("/admin/*")` scoped
|
|
85
|
+
* identically. Normalize a non-`*` pattern to have a leading slash before
|
|
86
|
+
* parsing so that behavior is preserved (without it, `parseRoutePattern("admin/*")`
|
|
87
|
+
* drops the static `admin` and the scope explodes to every path).
|
|
66
88
|
*/
|
|
67
|
-
export function
|
|
89
|
+
export function compileMiddlewarePattern(pattern: string): {
|
|
68
90
|
regex: RegExp;
|
|
69
91
|
paramNames: string[];
|
|
70
92
|
} {
|
|
@@ -72,47 +94,63 @@ export function parsePattern(pattern: string): {
|
|
|
72
94
|
return { regex: /^.*$/, paramNames: [] };
|
|
73
95
|
}
|
|
74
96
|
|
|
97
|
+
const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
98
|
+
const segments = parseRoutePattern(normalizedPattern);
|
|
75
99
|
const paramNames: string[] = [];
|
|
76
100
|
let regexStr = "^";
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
let hasTrailingWildcard = false;
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < segments.length; i++) {
|
|
104
|
+
const segment = segments[i];
|
|
105
|
+
|
|
106
|
+
if (segment.type === "wildcard") {
|
|
107
|
+
if (segment.value === "*") {
|
|
108
|
+
// Bare `*`: optional subtree match, no capture (parity with the original
|
|
109
|
+
// middleware parser). A trailing `*` matches the subtree; a non-trailing
|
|
110
|
+
// `*` matches zero-or-more intermediate segments, so `/a/<star>/b` still
|
|
111
|
+
// matches `/a/b`.
|
|
112
|
+
regexStr += "(?:/.*)?";
|
|
113
|
+
} else {
|
|
114
|
+
// Named catch-all `:name+` / `:name*`: capture the remainder under the
|
|
115
|
+
// name so a scoping middleware sees `ctx.params.<name>`, and respect the
|
|
116
|
+
// one-or-more arity (`+` must not match the bare prefix), mirroring the
|
|
117
|
+
// route matcher instead of collapsing to the bare-`*` subtree.
|
|
118
|
+
paramNames.push(segment.value);
|
|
119
|
+
regexStr += segment.oneOrMore ? "/(.+)" : "(?:/(.*))?";
|
|
120
|
+
}
|
|
121
|
+
if (i === segments.length - 1) {
|
|
122
|
+
hasTrailingWildcard = true;
|
|
123
|
+
}
|
|
124
|
+
} else if (segment.type === "param") {
|
|
125
|
+
paramNames.push(segment.value);
|
|
126
|
+
const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
|
|
127
|
+
const valuePattern = segment.constraint
|
|
128
|
+
? `(${segment.constraint.map(escapeRegExp).join("|")})`
|
|
129
|
+
: "([^/]+)";
|
|
130
|
+
if (segment.optional) {
|
|
131
|
+
regexStr += `(?:/${valuePattern}${suffixPattern})?`;
|
|
132
|
+
} else {
|
|
133
|
+
regexStr += `/${valuePattern}${suffixPattern}`;
|
|
134
|
+
}
|
|
91
135
|
} else {
|
|
92
|
-
//
|
|
93
|
-
regexStr += "/" +
|
|
136
|
+
// Static literal
|
|
137
|
+
regexStr += "/" + escapeRegExp(segment.value);
|
|
94
138
|
}
|
|
95
139
|
}
|
|
96
140
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
regexStr += "/?$";
|
|
100
|
-
} else {
|
|
101
|
-
regexStr += "$";
|
|
102
|
-
}
|
|
141
|
+
// Without a trailing `*`, match exactly with an optional trailing slash.
|
|
142
|
+
regexStr += hasTrailingWildcard ? "$" : "/?$";
|
|
103
143
|
|
|
104
144
|
return { regex: new RegExp(regexStr), paramNames };
|
|
105
145
|
}
|
|
106
146
|
|
|
107
147
|
/**
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Extract params from a pathname using a pattern's regex and param names
|
|
148
|
+
* Extract params from a pathname using a pattern's regex and param names.
|
|
149
|
+
*
|
|
150
|
+
* Values are URL-decoded so apps see the raw string (e.g. "ivo@example.com")
|
|
151
|
+
* instead of the percent-encoded form ("ivo%40example.com"). This matches the
|
|
152
|
+
* contract assumed by ctx.reverse (which re-encodes) and aligns with
|
|
153
|
+
* Express/React Router/Fastify/Koa.
|
|
116
154
|
*/
|
|
117
155
|
export function extractParams(
|
|
118
156
|
pathname: string,
|
|
@@ -124,7 +162,7 @@ export function extractParams(
|
|
|
124
162
|
|
|
125
163
|
const params: Record<string, string> = {};
|
|
126
164
|
for (let i = 0; i < paramNames.length; i++) {
|
|
127
|
-
params[paramNames[i]] = match[i + 1] || "";
|
|
165
|
+
params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
|
|
128
166
|
}
|
|
129
167
|
return params;
|
|
130
168
|
}
|
|
@@ -148,7 +186,15 @@ export function createMiddlewareContext<TEnv>(
|
|
|
148
186
|
search?: Record<string, unknown>,
|
|
149
187
|
) => string,
|
|
150
188
|
): MiddlewareContext<TEnv> {
|
|
151
|
-
|
|
189
|
+
// Reuse the request context's clean URL when available. Each middleware still
|
|
190
|
+
// gets its OWN URL clone (a middleware may mutate ctx.url / ctx.searchParams),
|
|
191
|
+
// but cloning the already-clean URL is cheaper than re-parsing + re-stripping
|
|
192
|
+
// request.url per middleware. Fall back to parsing when no ALS context.
|
|
193
|
+
const reqCtx = _getRequestContext();
|
|
194
|
+
const ctxUrl = reqCtx?.url;
|
|
195
|
+
const url = ctxUrl
|
|
196
|
+
? new URL(ctxUrl)
|
|
197
|
+
: stripInternalParams(new URL(request.url));
|
|
152
198
|
|
|
153
199
|
// Track the initial response to detect pre/post-next() phase.
|
|
154
200
|
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
@@ -179,14 +225,21 @@ export function createMiddlewareContext<TEnv>(
|
|
|
179
225
|
return responseHolder.response;
|
|
180
226
|
};
|
|
181
227
|
|
|
228
|
+
// reqCtx captured once above: the request-scoped platform fields
|
|
229
|
+
// (originalUrl, executionContext, waitUntil) are immutable per request,
|
|
230
|
+
// so snapshotting beats re-reading ALS on every access. The lazy getters
|
|
231
|
+
// below (routeName, theme, setTheme) stay lazy because those can change
|
|
232
|
+
// during `await next()`.
|
|
182
233
|
return {
|
|
183
234
|
request,
|
|
184
235
|
url,
|
|
185
|
-
originalUrl: new URL(request.url),
|
|
236
|
+
originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
|
|
186
237
|
pathname: url.pathname,
|
|
187
238
|
searchParams: url.searchParams,
|
|
188
239
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
189
240
|
params,
|
|
241
|
+
executionContext: reqCtx?.executionContext,
|
|
242
|
+
waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
|
|
190
243
|
// Getter: re-derives from request context on each access so that global
|
|
191
244
|
// middleware sees the matched route name after await next().
|
|
192
245
|
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
@@ -245,9 +298,13 @@ export function createMiddlewareContext<TEnv>(
|
|
|
245
298
|
|
|
246
299
|
reverse:
|
|
247
300
|
reverse ??
|
|
248
|
-
((
|
|
301
|
+
((
|
|
302
|
+
name: string,
|
|
303
|
+
_params?: Record<string, string>,
|
|
304
|
+
_search?: Record<string, unknown>,
|
|
305
|
+
) => {
|
|
249
306
|
throw new Error(
|
|
250
|
-
`ctx.reverse() is not available
|
|
307
|
+
`ctx.reverse(${JSON.stringify(name)}) is not available: no route map is bound to this middleware context.`,
|
|
251
308
|
);
|
|
252
309
|
}),
|
|
253
310
|
|
|
@@ -255,7 +312,10 @@ export function createMiddlewareContext<TEnv>(
|
|
|
255
312
|
const reqCtx = _getRequestContext();
|
|
256
313
|
if (reqCtx) {
|
|
257
314
|
reqCtx._debugPerformance = true;
|
|
258
|
-
reqCtx.
|
|
315
|
+
// Anchor to the true request entry (reqCtx._handlerStart) so phases that
|
|
316
|
+
// began before this opt-in report non-negative offsets; undefined falls
|
|
317
|
+
// back to performance.now() in createMetricsStore.
|
|
318
|
+
reqCtx._metricsStore ??= createMetricsStore(true, reqCtx._handlerStart);
|
|
259
319
|
}
|
|
260
320
|
},
|
|
261
321
|
};
|
|
@@ -281,9 +341,16 @@ export function matchMiddleware<TEnv>(
|
|
|
281
341
|
continue;
|
|
282
342
|
}
|
|
283
343
|
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
344
|
+
// Run the scope regex ONCE per entry. The old code ran test() then, on a
|
|
345
|
+
// hit, extractParams' match() — a second full pass over the same string for
|
|
346
|
+
// every matching entry on the per-request hot path. The regexes carry no
|
|
347
|
+
// `g` flag, so there is no lastIndex statefulness across this single match.
|
|
348
|
+
const m = pathname.match(entry.regex);
|
|
349
|
+
if (m) {
|
|
350
|
+
const params: Record<string, string> = {};
|
|
351
|
+
for (let i = 0; i < entry.paramNames.length; i++) {
|
|
352
|
+
params[entry.paramNames[i]] = safeDecodeURIComponent(m[i + 1] || "");
|
|
353
|
+
}
|
|
287
354
|
matches.push({ entry, params });
|
|
288
355
|
}
|
|
289
356
|
}
|
|
@@ -291,6 +358,88 @@ export function matchMiddleware<TEnv>(
|
|
|
291
358
|
return matches;
|
|
292
359
|
}
|
|
293
360
|
|
|
361
|
+
// Set-Cookie is appended; for other headers stubOverridesNonCookie=true
|
|
362
|
+
// overwrites (chain ran to completion), false fills only missing slots (an
|
|
363
|
+
// explicit short-circuit Response's own headers win).
|
|
364
|
+
function mergeStubHeaders(
|
|
365
|
+
target: Headers,
|
|
366
|
+
stub: Headers,
|
|
367
|
+
stubOverridesNonCookie: boolean,
|
|
368
|
+
): void {
|
|
369
|
+
stub.forEach((value, name) => {
|
|
370
|
+
// The reserved external-redirect marker is internal and never a trust
|
|
371
|
+
// signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
|
|
372
|
+
// browser-facing response. The opt-in is the out-of-band brand.
|
|
373
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
374
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
375
|
+
target.append(name, value);
|
|
376
|
+
} else if (stubOverridesNonCookie || !target.has(name)) {
|
|
377
|
+
target.set(name, value);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Set-Cookie is deduped so a nested inner executeMiddleware that already merged
|
|
383
|
+
// the same reqCtx cookies does not duplicate them; other headers fill if missing.
|
|
384
|
+
function mergeReqCtxStub(
|
|
385
|
+
target: Headers,
|
|
386
|
+
reqCtx: ReturnType<typeof _getRequestContext>,
|
|
387
|
+
): void {
|
|
388
|
+
if (!reqCtx) return;
|
|
389
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
390
|
+
if (stubCookies.length > 0) {
|
|
391
|
+
const existing = new Set(target.getSetCookie());
|
|
392
|
+
for (const cookie of stubCookies) {
|
|
393
|
+
if (!existing.has(cookie)) {
|
|
394
|
+
target.append("set-cookie", cookie);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
399
|
+
// Never propagate the reserved external-redirect marker (see mergeStubHeaders).
|
|
400
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
401
|
+
if (name !== "set-cookie" && !target.has(name)) {
|
|
402
|
+
target.set(name, value);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Clone `base` with stub headers merged into a fresh Headers (the clone keeps
|
|
408
|
+
// the body mutable for post-next() modifications). Set-Cookie is always
|
|
409
|
+
// appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
|
|
410
|
+
// mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
|
|
411
|
+
// short-circuit path passes false (its reqCtx headers are not merged here),
|
|
412
|
+
// which is the one deliberate divergence between the call sites.
|
|
413
|
+
function mergeResponse(
|
|
414
|
+
base: Response,
|
|
415
|
+
stub: Headers,
|
|
416
|
+
opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
|
|
417
|
+
): Response {
|
|
418
|
+
const mergedHeaders = new Headers(base.headers);
|
|
419
|
+
// The reserved external-redirect marker is never a trust signal and must never
|
|
420
|
+
// reach the browser. The guard strips it on 3xx redirects; strip it here too so
|
|
421
|
+
// a forged value cannot ride a non-3xx middleware response (which the 3xx-only
|
|
422
|
+
// guard would not touch) to the client. The opt-in is the out-of-band brand.
|
|
423
|
+
mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
|
|
424
|
+
mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
|
|
425
|
+
if (opts.mergeReqCtx) {
|
|
426
|
+
mergeReqCtxStub(mergedHeaders, _getRequestContext());
|
|
427
|
+
}
|
|
428
|
+
const merged = new Response(base.body, {
|
|
429
|
+
status: base.status,
|
|
430
|
+
statusText: base.statusText,
|
|
431
|
+
headers: mergedHeaders,
|
|
432
|
+
});
|
|
433
|
+
// Transfer the out-of-band external-redirect brand across this rebuild: a
|
|
434
|
+
// middleware short-circuit `return redirect(url, { external: true })` reaches
|
|
435
|
+
// the open-redirect guard only after this merge, and the brand lives on the
|
|
436
|
+
// Response object, not in its headers.
|
|
437
|
+
if (isExternalRedirect(base)) {
|
|
438
|
+
markExternalRedirect(merged);
|
|
439
|
+
}
|
|
440
|
+
return merged;
|
|
441
|
+
}
|
|
442
|
+
|
|
294
443
|
/**
|
|
295
444
|
* Execute middleware chain
|
|
296
445
|
*
|
|
@@ -299,7 +448,7 @@ export function matchMiddleware<TEnv>(
|
|
|
299
448
|
* - `ctx.headers` available before and after `await next()`
|
|
300
449
|
* - `ctx.header()` shorthand for setting a single header
|
|
301
450
|
* - Forgiving: if middleware doesn't return, uses the downstream response
|
|
302
|
-
* - Short-circuit: return Response to stop chain
|
|
451
|
+
* - Short-circuit: return OR throw a Response to stop chain
|
|
303
452
|
* - Error catching: try/catch around `next()` works
|
|
304
453
|
*/
|
|
305
454
|
export async function executeMiddleware<TEnv>(
|
|
@@ -329,42 +478,16 @@ export async function executeMiddleware<TEnv>(
|
|
|
329
478
|
// End of chain - call actual RSC handler
|
|
330
479
|
const response = await finalHandler();
|
|
331
480
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
stubResponse.headers.forEach((value, name) => {
|
|
336
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
337
|
-
mergedHeaders.append(name, value);
|
|
338
|
-
} else {
|
|
339
|
-
mergedHeaders.set(name, value);
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
// Also merge shared RequestContext stub (cookies written via cookies().set()).
|
|
343
|
-
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
344
|
-
// may have already merged the same reqCtx cookies into the response.
|
|
345
|
-
const reqCtx = _getRequestContext();
|
|
346
|
-
if (reqCtx) {
|
|
347
|
-
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
348
|
-
if (stubCookies.length > 0) {
|
|
349
|
-
const existing = new Set(mergedHeaders.getSetCookie());
|
|
350
|
-
for (const cookie of stubCookies) {
|
|
351
|
-
if (!existing.has(cookie)) {
|
|
352
|
-
mergedHeaders.append("set-cookie", cookie);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
357
|
-
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
358
|
-
mergedHeaders.set(name, value);
|
|
359
|
-
}
|
|
360
|
-
});
|
|
481
|
+
if (isWebSocketUpgradeResponse(response)) {
|
|
482
|
+
responseHolder.response = response;
|
|
483
|
+
return response;
|
|
361
484
|
}
|
|
362
485
|
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
486
|
+
// Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
|
|
487
|
+
// and reqCtx stub headers are merged in.
|
|
488
|
+
responseHolder.response = mergeResponse(response, stubResponse.headers, {
|
|
489
|
+
stubOverridesNonCookie: true,
|
|
490
|
+
mergeReqCtx: true,
|
|
368
491
|
});
|
|
369
492
|
|
|
370
493
|
return responseHolder.response;
|
|
@@ -422,14 +545,36 @@ export async function executeMiddleware<TEnv>(
|
|
|
422
545
|
return nextPromise;
|
|
423
546
|
};
|
|
424
547
|
|
|
548
|
+
// Wrap the middleware (including its downstream next() chain) in its span
|
|
549
|
+
// via the unified phase API. metric:false — the middleware's perf metric is
|
|
550
|
+
// its exclusive pre/post own-time, recorded directly above and below, finer
|
|
551
|
+
// than a single wrap. Spans nest by async context, so this onions
|
|
552
|
+
// middleware-over-middleware and the core handler underneath. Pass-through
|
|
553
|
+
// when neither surface is active.
|
|
425
554
|
let result: Response | void;
|
|
426
555
|
try {
|
|
427
|
-
result = await
|
|
428
|
-
|
|
556
|
+
result = await observePhase(PHASES.middleware(metricLabel), async () => {
|
|
557
|
+
try {
|
|
558
|
+
return await entry.handler(ctx, wrappedNext);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
// Thrown Response is short-circuit control flow, not an error —
|
|
561
|
+
// absorb it INSIDE the span so the tracing runner settles the
|
|
562
|
+
// rango.middleware span as success, not STATUS_ERROR (every auth
|
|
563
|
+
// redirect would otherwise inflate trace error rates). Returning it
|
|
564
|
+
// routes through the `if (result instanceof Response)` branch below,
|
|
565
|
+
// so stub headers and request-context cookies merge identically to an
|
|
566
|
+
// explicit `return new Response(...)`. Segment handlers already follow
|
|
567
|
+
// this convention (segment-resolution/helpers.ts keeps result handling
|
|
568
|
+
// outside the span). Real errors propagate past the span.
|
|
569
|
+
if (error instanceof Response) return error;
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
} finally {
|
|
574
|
+
// Settle the middleware own-time metric once on both the success and
|
|
575
|
+
// error paths (idempotent guard in finishMiddleware).
|
|
429
576
|
finishMiddleware();
|
|
430
|
-
throw error;
|
|
431
577
|
}
|
|
432
|
-
finishMiddleware();
|
|
433
578
|
|
|
434
579
|
// Record post-next() processing time when middleware did work after
|
|
435
580
|
// the downstream chain resolved (e.g. adding headers, logging).
|
|
@@ -448,41 +593,18 @@ export async function executeMiddleware<TEnv>(
|
|
|
448
593
|
|
|
449
594
|
// Explicit return takes precedence (middleware short-circuit).
|
|
450
595
|
// Merge stub headers (from ctx.header before this point) and
|
|
451
|
-
// RequestContext stub headers (from
|
|
596
|
+
// RequestContext stub headers (from cookies().set()) into the
|
|
452
597
|
// returned Response so they are not lost.
|
|
453
598
|
if (result instanceof Response) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
mergedHeaders.append(name, value);
|
|
458
|
-
} else if (!mergedHeaders.has(name)) {
|
|
459
|
-
mergedHeaders.set(name, value);
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
// Also merge shared RequestContext stub (cookies written via setCookie).
|
|
463
|
-
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
464
|
-
// may have already merged the same reqCtx cookies into the response.
|
|
465
|
-
const reqCtx = _getRequestContext();
|
|
466
|
-
if (reqCtx) {
|
|
467
|
-
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
468
|
-
if (stubCookies.length > 0) {
|
|
469
|
-
const existing = new Set(mergedHeaders.getSetCookie());
|
|
470
|
-
for (const cookie of stubCookies) {
|
|
471
|
-
if (!existing.has(cookie)) {
|
|
472
|
-
mergedHeaders.append("set-cookie", cookie);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
477
|
-
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
478
|
-
mergedHeaders.set(name, value);
|
|
479
|
-
}
|
|
480
|
-
});
|
|
599
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
600
|
+
responseHolder.response = result;
|
|
601
|
+
return result;
|
|
481
602
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
603
|
+
// Explicit short-circuit: the returned Response's own headers win
|
|
604
|
+
// (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
|
|
605
|
+
const merged = mergeResponse(result, stubResponse.headers, {
|
|
606
|
+
stubOverridesNonCookie: false,
|
|
607
|
+
mergeReqCtx: true,
|
|
486
608
|
});
|
|
487
609
|
responseHolder.response = merged;
|
|
488
610
|
return merged;
|
|
@@ -527,23 +649,12 @@ export async function executeMiddleware<TEnv>(
|
|
|
527
649
|
// last merge point (e.g. cookies().set() called after await next()).
|
|
528
650
|
// The reqCtx stub may have already been partially merged during finalHandler
|
|
529
651
|
// or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
|
|
652
|
+
//
|
|
653
|
+
// Skip for upgrade responses: upgrade headers are semantically immutable and
|
|
654
|
+
// set-cookie on an upgrade is not meaningful.
|
|
530
655
|
const reqCtx = _getRequestContext();
|
|
531
|
-
if (reqCtx) {
|
|
532
|
-
|
|
533
|
-
if (stubCookies.length > 0) {
|
|
534
|
-
const existingCookies = new Set(finalResponse.headers.getSetCookie());
|
|
535
|
-
for (const cookie of stubCookies) {
|
|
536
|
-
if (!existingCookies.has(cookie)) {
|
|
537
|
-
finalResponse.headers.append("set-cookie", cookie);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
// Fill in non-cookie headers that aren't already on the response
|
|
542
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
543
|
-
if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
|
|
544
|
-
finalResponse.headers.set(name, value);
|
|
545
|
-
}
|
|
546
|
-
});
|
|
656
|
+
if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
|
|
657
|
+
mergeReqCtxStub(finalResponse.headers, reqCtx);
|
|
547
658
|
}
|
|
548
659
|
|
|
549
660
|
return finalResponse;
|
|
@@ -554,7 +665,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
554
665
|
*
|
|
555
666
|
* Intercepts use a shared stubResponse from the request context. This function:
|
|
556
667
|
* - Runs middleware in sequence with a simple next() chain
|
|
557
|
-
* - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
|
|
668
|
+
* - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
|
|
558
669
|
* - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
|
|
559
670
|
*
|
|
560
671
|
* @param middlewares - Array of middleware functions
|
|
@@ -592,6 +703,7 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
592
703
|
return stubResponse;
|
|
593
704
|
}
|
|
594
705
|
|
|
706
|
+
const ordinal = index;
|
|
595
707
|
const middleware = middlewares[index++];
|
|
596
708
|
const ctx = createMiddlewareContext(
|
|
597
709
|
request,
|
|
@@ -613,7 +725,30 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
613
725
|
return next();
|
|
614
726
|
};
|
|
615
727
|
|
|
616
|
-
|
|
728
|
+
// Span-wrap each intercept middleware as rango.middleware (metric:false —
|
|
729
|
+
// intercept runs inside the render phase already metered by render:total, so
|
|
730
|
+
// it contributes a span but no separate perf metric). Bare MiddlewareFns
|
|
731
|
+
// have no pattern, so the label is scoped to "*" like a pattern-less entry.
|
|
732
|
+
const label = getMiddlewareMetricLabel(
|
|
733
|
+
{ handler: middleware, pattern: null } as MiddlewareEntry<TEnv>,
|
|
734
|
+
ordinal,
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
const result: Response | void = await observePhase(
|
|
738
|
+
PHASES.middleware(label),
|
|
739
|
+
async () => {
|
|
740
|
+
try {
|
|
741
|
+
return await middleware(ctx, guardedNext);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
// Thrown Response is short-circuit control flow, parity with the
|
|
744
|
+
// explicit-return path below. Absorb it INSIDE the span so the tracing
|
|
745
|
+
// runner settles rango.middleware as success, not STATUS_ERROR (same
|
|
746
|
+
// reasoning as executeMiddleware's main chain). Real errors propagate.
|
|
747
|
+
if (error instanceof Response) return error;
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
);
|
|
617
752
|
|
|
618
753
|
if (result instanceof Response) {
|
|
619
754
|
earlyResponse = result;
|
|
@@ -637,21 +772,13 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
637
772
|
});
|
|
638
773
|
|
|
639
774
|
if (hasStubHeaders) {
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
|
|
644
|
-
stubResponse.headers
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
} else if (!mergedHeaders.has(name)) {
|
|
648
|
-
mergedHeaders.set(name, value);
|
|
649
|
-
}
|
|
650
|
-
});
|
|
651
|
-
return new Response(response.body, {
|
|
652
|
-
status: response.status,
|
|
653
|
-
statusText: response.statusText,
|
|
654
|
-
headers: mergedHeaders,
|
|
775
|
+
// Only fill in missing headers — the returned Response's explicit headers
|
|
776
|
+
// take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
|
|
777
|
+
// mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
|
|
778
|
+
// stub headers here (pinned by intercept-middleware-headers.test.ts).
|
|
779
|
+
return mergeResponse(response, stubResponse.headers, {
|
|
780
|
+
stubOverridesNonCookie: false,
|
|
781
|
+
mergeReqCtx: false,
|
|
655
782
|
});
|
|
656
783
|
}
|
|
657
784
|
return response;
|
|
@@ -692,7 +819,6 @@ export async function executeLoaderMiddleware<TEnv>(
|
|
|
692
819
|
regex: null,
|
|
693
820
|
paramNames: [],
|
|
694
821
|
handler,
|
|
695
|
-
mountPrefix: null,
|
|
696
822
|
} as MiddlewareEntry<TEnv>,
|
|
697
823
|
params,
|
|
698
824
|
}));
|