@rangojs/router 0.26.6 → 0.28.0
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/dist/bin/rango.js +1 -1
- package/dist/types/__internal.d.ts +7 -1
- package/dist/types/browser/action-coordinator.d.ts +0 -4
- package/dist/types/browser/action-fence.d.ts +11 -3
- package/dist/types/browser/event-controller.d.ts +20 -1
- package/dist/types/browser/history-state.d.ts +7 -9
- package/dist/types/browser/intercept-utils.d.ts +23 -0
- package/dist/types/browser/navigation-store.d.ts +8 -6
- package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/segment-reconciler.d.ts +6 -4
- package/dist/types/browser/types.d.ts +2 -1
- package/dist/types/browser/wire-protocol.d.ts +25 -0
- package/dist/types/cache/cache-key-utils.d.ts +11 -5
- package/dist/types/cache/cache-scope.d.ts +34 -22
- package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
- package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
- package/dist/types/cache/handle-snapshot.d.ts +9 -4
- package/dist/types/cache/segment-codec.d.ts +14 -0
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/tag-invalidation.d.ts +10 -0
- package/dist/types/cache/taint.d.ts +5 -3
- package/dist/types/cache/types.d.ts +7 -3
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
- package/dist/types/prefetch-markers.d.ts +4 -6
- package/dist/types/prerender/store.d.ts +31 -0
- package/dist/types/prerender/warm-request.d.ts +3 -3
- package/dist/types/prerender.d.ts +2 -2
- package/dist/types/route-name.d.ts +5 -0
- package/dist/types/router/error-handling.d.ts +4 -4
- package/dist/types/router/instrument.d.ts +4 -3
- package/dist/types/router/intercept-resolution.d.ts +25 -9
- package/dist/types/router/match-context.d.ts +12 -11
- package/dist/types/router/match-handlers.d.ts +2 -6
- package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
- package/dist/types/router/match-middleware/cache-lookup.d.ts +35 -87
- package/dist/types/router/match-middleware/cache-store.d.ts +2 -2
- package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
- package/dist/types/router/prerender-match.d.ts +21 -6
- package/dist/types/router/preview-match.d.ts +2 -7
- package/dist/types/router/request-classification.d.ts +5 -0
- package/dist/types/router/revalidation.d.ts +6 -1
- package/dist/types/router/router-context.d.ts +9 -63
- package/dist/types/router/router-interfaces.d.ts +8 -14
- package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
- package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
- package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
- package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
- package/dist/types/router/segment-resolution.d.ts +1 -2
- package/dist/types/router/segment-wrappers.d.ts +11 -48
- package/dist/types/router/telemetry.d.ts +2 -0
- package/dist/types/router/types.d.ts +10 -0
- package/dist/types/rsc/full-payload.d.ts +2 -2
- package/dist/types/rsc/handler-context.d.ts +6 -6
- package/dist/types/rsc/json-route-result.d.ts +5 -9
- package/dist/types/rsc/request-stages.d.ts +203 -0
- package/dist/types/rsc/response-cache-serve.d.ts +7 -12
- package/dist/types/rsc/response-route-handler.d.ts +2 -2
- package/dist/types/rsc/routine-plan.d.ts +1 -1
- package/dist/types/rsc/server-action.d.ts +2 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
- package/dist/types/rsc/shell-capture.d.ts +132 -15
- package/dist/types/rsc/shell-serve.d.ts +37 -12
- package/dist/types/server/context.d.ts +9 -8
- package/dist/types/server/handle-store.d.ts +16 -0
- package/dist/types/server/render-pass.d.ts +72 -0
- package/dist/types/server/request-context.d.ts +24 -104
- package/dist/types/server/rewrite-trail.d.ts +2 -2
- package/dist/types/testing/dispatch.d.ts +35 -18
- package/dist/types/testing/serve-shell-request.d.ts +7 -0
- package/dist/types/types/cache-types.d.ts +6 -3
- package/dist/types/types/error-types.d.ts +9 -1
- package/dist/types/types/handler-context.d.ts +18 -7
- package/dist/types/types/segments.d.ts +1 -1
- package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
- package/dist/types/vite/discovery/build-env.d.ts +22 -0
- package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
- package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
- package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
- package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +12 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
- package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +8 -47
- package/dist/types/vite/temp-rsc-server.d.ts +39 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
- package/dist/types/wire-params.d.ts +34 -0
- package/dist/vite/index.js +3809 -3710
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +8 -5
- package/skills/caching/SKILL.md +37 -18
- package/skills/client-urls/SKILL.md +2 -1
- package/skills/document-cache/SKILL.md +18 -1
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +53 -13
- package/skills/layout/SKILL.md +11 -7
- package/skills/loader/SKILL.md +30 -13
- package/skills/middleware/SKILL.md +19 -1
- package/skills/migrate-nextjs/SKILL.md +5 -4
- package/skills/parallel/SKILL.md +5 -4
- package/skills/ppr/SKILL.md +19 -3
- package/skills/prerender/SKILL.md +39 -15
- package/skills/rango/SKILL.md +23 -11
- package/skills/server-actions/SKILL.md +51 -6
- package/skills/testing/predicates.md +1 -1
- package/skills/testing/response-routes.md +6 -4
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/use-cache/SKILL.md +46 -8
- package/skills/ux-architecture/SKILL.md +3 -2
- package/src/__internal.ts +7 -1
- package/src/browser/action-coordinator.ts +2 -13
- package/src/browser/action-fence.ts +11 -3
- package/src/browser/connection-warmup.ts +3 -1
- package/src/browser/event-controller.ts +42 -3
- package/src/browser/history-state.ts +15 -13
- package/src/browser/intercept-utils.ts +51 -0
- package/src/browser/navigation-bridge.ts +34 -30
- package/src/browser/navigation-client.ts +21 -33
- package/src/browser/navigation-store.ts +25 -27
- package/src/browser/navigation-transaction.ts +7 -11
- package/src/browser/partial-update.ts +8 -0
- package/src/browser/prefetch/cache.ts +2 -1
- package/src/browser/prefetch/fetch.ts +8 -16
- package/src/browser/prefetch/queue.ts +4 -4
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/filter-segment-order.ts +2 -2
- package/src/browser/rsc-router.tsx +17 -2
- package/src/browser/segment-reconciler.ts +66 -53
- package/src/browser/server-action-bridge.ts +265 -127
- package/src/browser/types.ts +2 -1
- package/src/browser/wire-protocol.ts +41 -0
- package/src/cache/cache-key-utils.ts +11 -5
- package/src/cache/cache-runtime.ts +110 -50
- package/src/cache/cache-scope.ts +110 -64
- package/src/cache/cf/cf-cache-constants.ts +2 -2
- package/src/cache/cf/cf-cache-store.ts +486 -874
- package/src/cache/cf/cf-kv-utils.ts +1 -1
- package/src/cache/document-cache.ts +42 -15
- package/src/cache/handle-snapshot.ts +19 -7
- package/src/cache/segment-codec.ts +55 -0
- package/src/cache/shell-snapshot.ts +2 -2
- package/src/cache/tag-invalidation.ts +21 -2
- package/src/cache/taint.ts +5 -3
- package/src/cache/types.ts +7 -3
- package/src/cache/vercel/vercel-cache-store.ts +243 -310
- package/src/handles/pushed-by.ts +1 -1
- package/src/prefetch-markers.ts +4 -7
- package/src/prerender/store.ts +76 -0
- package/src/prerender/warm-request.ts +8 -4
- package/src/prerender.ts +2 -2
- package/src/route-name.ts +8 -0
- package/src/router/error-handling.ts +4 -4
- package/src/router/instrument.ts +4 -3
- package/src/router/intercept-resolution.ts +77 -75
- package/src/router/loader-resolution.ts +5 -4
- package/src/router/match-api.ts +4 -5
- package/src/router/match-context.ts +33 -12
- package/src/router/match-handlers.ts +8 -20
- package/src/router/match-middleware/background-revalidation.ts +41 -53
- package/src/router/match-middleware/cache-lookup.ts +680 -720
- package/src/router/match-middleware/cache-store.ts +14 -12
- package/src/router/match-middleware/intercept-resolution.ts +3 -23
- package/src/router/match-middleware/segment-resolution.ts +34 -61
- package/src/router/match-result.ts +1 -20
- package/src/router/middleware.ts +69 -11
- package/src/router/navigation-snapshot.ts +4 -3
- package/src/router/prerender-match.ts +100 -44
- package/src/router/preview-match.ts +6 -41
- package/src/router/request-classification.ts +13 -7
- package/src/router/revalidation.ts +33 -7
- package/src/router/route-snapshot.ts +2 -5
- package/src/router/router-context.ts +14 -117
- package/src/router/router-interfaces.ts +8 -14
- package/src/router/segment-resolution/loader-cache.ts +14 -9
- package/src/router/segment-resolution/loader-mask.ts +5 -5
- package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
- package/src/router/segment-resolution/resolution-pass.ts +137 -0
- package/src/router/segment-resolution/revalidation.ts +897 -776
- package/src/router/segment-resolution.ts +4 -13
- package/src/router/segment-wrappers.ts +51 -269
- package/src/router/telemetry.ts +5 -0
- package/src/router/types.ts +10 -0
- package/src/router.ts +12 -13
- package/src/rsc/full-payload.ts +6 -5
- package/src/rsc/handler-context.ts +8 -14
- package/src/rsc/handler.ts +126 -507
- package/src/rsc/json-route-result.ts +5 -9
- package/src/rsc/loader-fetch.ts +6 -5
- package/src/rsc/progressive-enhancement.ts +5 -0
- package/src/rsc/request-stages.ts +757 -0
- package/src/rsc/response-cache-serve.ts +10 -14
- package/src/rsc/response-route-handler.ts +13 -5
- package/src/rsc/rewrite-passes.ts +2 -1
- package/src/rsc/routine-plan.ts +1 -1
- package/src/rsc/rsc-rendering.ts +172 -349
- package/src/rsc/server-action.ts +87 -113
- package/src/rsc/shell-capture-constants.ts +3 -1
- package/src/rsc/shell-capture.ts +212 -169
- package/src/rsc/shell-serve.ts +121 -18
- package/src/rsc/ssr-setup.ts +5 -4
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +22 -25
- package/src/server/handle-store.ts +55 -0
- package/src/server/render-pass.ts +291 -0
- package/src/server/request-context.ts +26 -115
- package/src/server/rewrite-trail.ts +6 -4
- package/src/ssr/index.tsx +1 -1
- package/src/ssr/ssr-root.tsx +1 -0
- package/src/testing/dispatch.ts +223 -710
- package/src/testing/e2e/page-helpers.ts +2 -2
- package/src/testing/serve-shell-request.ts +37 -32
- package/src/types/cache-types.ts +6 -3
- package/src/types/error-types.ts +9 -1
- package/src/types/handler-context.ts +21 -7
- package/src/types/segments.ts +1 -1
- package/src/vite/discovery/build-discovery-phase.ts +204 -0
- package/src/vite/discovery/build-env.ts +148 -0
- package/src/vite/discovery/bundle-scan-phase.ts +128 -0
- package/src/vite/discovery/dev-prerender-cache.ts +8 -3
- package/src/vite/discovery/dev-server-phase.ts +1776 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +24 -39
- package/src/vite/discovery/state.ts +15 -0
- package/src/vite/plugins/version-plugin.ts +1 -1
- package/src/vite/plugins/virtual-entries.ts +1 -1
- package/src/vite/router-discovery.ts +29 -2428
- package/src/vite/temp-rsc-server.ts +223 -0
- package/src/vite/utils/prerender-utils.ts +47 -0
- package/src/wire-params.ts +35 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
- package/src/router/segment-resolution/fresh.ts +0 -945
package/src/testing/dispatch.ts
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* dispatch — in-process request -> Response for unit/integration tests,
|
|
3
3
|
* WITHOUT the Flight RSC runtime.
|
|
4
4
|
*
|
|
5
|
-
* dispatch runs the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* dispatch runs the production handler's request stages
|
|
6
|
+
* (rsc/request-stages.ts: classify, the terminal plans, the global middleware
|
|
7
|
+
* passes, the response route and the response's finalization) so that
|
|
8
|
+
* redirects, 404s, reloads, response routes (path.json / path.text /
|
|
9
|
+
* path.html / ...), and middleware short-circuits behave exactly as in
|
|
10
|
+
* production. It deliberately
|
|
8
11
|
* does NOT render React Server Components: there is no Flight stream, no SSR,
|
|
9
12
|
* and no DOM. Hit an RSC (component) route and dispatch throws a clear error
|
|
10
13
|
* directing you to renderToFlightString/renderServerTree/renderHandler or an e2e test.
|
|
@@ -17,19 +20,20 @@
|
|
|
17
20
|
* so a global auth middleware can 401/redirect them and middleware-set
|
|
18
21
|
* cookies/headers merge onto the 308/404 the way createResponseWithMergedHeaders
|
|
19
22
|
* merges them in production.
|
|
20
|
-
* - Response routes (non-RSC)
|
|
23
|
+
* - Response routes (non-RSC), served by production's handleResponseRoute
|
|
24
|
+
* (rsc/response-route-handler.ts) -> serialized Response
|
|
21
25
|
* - json: JSON.stringify(result) (bare value) with application/json
|
|
22
26
|
* - text/html/xml/md: String(result) with the mapped MIME type
|
|
23
|
-
* - handler returning or throwing a Response: re-wrapped
|
|
24
|
-
*
|
|
25
|
-
*
|
|
27
|
+
* - handler returning or throwing a Response: re-wrapped (stub
|
|
28
|
+
* headers/cookies merged, Set-Cookie preserved, WebSocket upgrade passed
|
|
29
|
+
* through without reconstruction)
|
|
26
30
|
* - handler throwing a non-Response error: typed 500 / RouterError
|
|
27
|
-
* status
|
|
28
|
-
*
|
|
31
|
+
* status (RFC 9457 problem+json body with application/problem+json for
|
|
32
|
+
* json routes, text/plain message otherwise)
|
|
29
33
|
* - content-negotiated route: Vary: Accept appended
|
|
30
34
|
* - cached response route (cache({...})): getResponse/putResponse
|
|
31
|
-
* hit/SWR/tag write, resolved from the matched entry tree
|
|
32
|
-
*
|
|
35
|
+
* hit/SWR/tag write, resolved from the matched entry tree. The write is
|
|
36
|
+
* scheduled via ctx.waitUntil
|
|
33
37
|
* (a microtask without an executionContext), so a HIT-asserting test must
|
|
34
38
|
* flush microtasks between the seeding dispatch and the asserting one.
|
|
35
39
|
* - Global middleware (router.use(...)) AND route-level middleware, with full
|
|
@@ -38,10 +42,17 @@
|
|
|
38
42
|
* global middleware runs first (so an auth gate can still 401/redirect),
|
|
39
43
|
* then — if it passes through — an X-RSC-Reload is returned. Route-level
|
|
40
44
|
* middleware is skipped on a partial, exactly as production skips it.
|
|
41
|
-
* - A middleware redirect (3xx + Location) on a partial/action request
|
|
42
|
-
* (?_rsc_partial / ?_rsc_action)
|
|
43
|
-
*
|
|
45
|
+
* - A global middleware redirect (3xx + Location) on a partial/action request
|
|
46
|
+
* (?_rsc_partial / ?_rsc_action), and a findMatch() redirect on a partial
|
|
47
|
+
* request: a 204 + X-RSC-Redirect, so fetch() does not auto-follow the 3xx —
|
|
44
48
|
* identical to production's no-location-state path.
|
|
49
|
+
* - The terminal plans production answers inside the global chain
|
|
50
|
+
* (terminalPlanResponse): a `_rsc_v` version mismatch and a `_rsc_rid` app
|
|
51
|
+
* switch reload (X-RSC-Reload), and an action (?_rsc_action) a rewrite moved
|
|
52
|
+
* to a target that is not a page route throws production's error.
|
|
53
|
+
* - Route params and name are set inside the global chain, after classify, by
|
|
54
|
+
* the stage production's executeRequest runs (recordPlanRoute): global
|
|
55
|
+
* middleware sees them after next(), not before it.
|
|
45
56
|
* - The open-redirect guard (rsc/redirect-guard.ts) on full (browser-followed)
|
|
46
57
|
* redirects: a cross-origin Location is rewritten to the basename root unless
|
|
47
58
|
* redirect(url, { external: true }) opted out, mirroring production's single
|
|
@@ -60,7 +71,9 @@
|
|
|
60
71
|
* after finalizeResponse (segmentCount 0 / cacheHit false — dispatch renders no
|
|
61
72
|
* RSC segments and holds no match-cache state), and a thrown non-Response error
|
|
62
73
|
* emits request.error with phase "routing". All three carry the same requestId
|
|
63
|
-
* (getRequestId)
|
|
74
|
+
* (getRequestId), as does a response-route timeout on a rewritten pass
|
|
75
|
+
* (runRequestPasses carries the id to each pass's request, as in
|
|
76
|
+
* production). Emission is gated entirely on a configured sink; with none,
|
|
64
77
|
* dispatch does zero new work and stays byte-identical for existing callers.
|
|
65
78
|
* Lets a consumer unit-test their sink wiring in-process instead of only at e2e.
|
|
66
79
|
* - Response-route `renderStartMs` timeouts and `onTimeout`: dispatch races the
|
|
@@ -96,26 +109,30 @@
|
|
|
96
109
|
* request.start/end/error lifecycle above; cover cache/loader telemetry with an
|
|
97
110
|
* e2e test driving a real RSC request.
|
|
98
111
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* route
|
|
112
|
+
* Each pass is classified with the handler's classify stage
|
|
113
|
+
* (router/request-classification.ts classifyRequest), which runs content
|
|
114
|
+
* negotiation and resolves route middleware from the matched entry tree, so
|
|
115
|
+
* dispatch's route-middleware collection is exactly the router's, not a
|
|
116
|
+
* re-implementation. The origin guard is not run: it gates only the action,
|
|
117
|
+
* loader and PE-form plans, which classify gives a component route only, and
|
|
118
|
+
* dispatch throws on those before the pass's middleware.
|
|
102
119
|
*/
|
|
103
120
|
|
|
121
|
+
import type {
|
|
122
|
+
ClassifyContext,
|
|
123
|
+
RequestStageContext,
|
|
124
|
+
} from "../rsc/request-stages.js";
|
|
125
|
+
import type {
|
|
126
|
+
ClassifyRequestDeps,
|
|
127
|
+
ExecutableRequestPlan,
|
|
128
|
+
RequestPlan,
|
|
129
|
+
} from "../router/request-classification.js";
|
|
104
130
|
import {
|
|
105
131
|
createRequestContext,
|
|
106
132
|
runWithRequestContext,
|
|
107
|
-
setRequestContextParams,
|
|
108
133
|
} from "../server/request-context.js";
|
|
109
|
-
import { REWRITE_HEADER, runRewritePasses } from "../rsc/rewrite-passes.js";
|
|
110
|
-
import { defineRewrittenFrom } from "../server/rewrite-trail.js";
|
|
111
134
|
import { resolveRouterVersions } from "../server/build-version-table.js";
|
|
112
|
-
import {
|
|
113
|
-
import type {
|
|
114
|
-
MiddlewareEntry,
|
|
115
|
-
MiddlewareFn,
|
|
116
|
-
} from "../router/middleware-types.js";
|
|
117
|
-
import { createReverseFunction } from "../router/handler-context.js";
|
|
118
|
-
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
135
|
+
import type { MiddlewareEntry } from "../router/middleware-types.js";
|
|
119
136
|
import {
|
|
120
137
|
compileSearchParamsFilter,
|
|
121
138
|
type CacheSearchParams,
|
|
@@ -123,40 +140,17 @@ import {
|
|
|
123
140
|
} from "../cache/search-params-filter.js";
|
|
124
141
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
125
142
|
import type { CacheProfile } from "../cache/profile-registry.js";
|
|
126
|
-
// cache-scope is loaded LAZILY inside the response-route cache path (below):
|
|
127
|
-
// its module graph pulls @vitejs/plugin-rsc/rsc (via segment-codec), which the
|
|
128
|
-
// non-Vite unit-test runner cannot resolve. A static import here would drag that
|
|
129
|
-
// onto the whole testing barrel's eager graph and break every consumer suite
|
|
130
|
-
// that imports `@rangojs/router/testing` without mocking plugin-rsc.
|
|
131
|
-
import type { EntryData } from "../server/context.js";
|
|
132
143
|
import { ensureRouterTrie } from "../rsc/manifest-init.js";
|
|
133
|
-
import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
|
|
134
|
-
import { RouterError } from "../errors.js";
|
|
135
|
-
import { createProblemDetails } from "../rsc/response-error.js";
|
|
136
144
|
import {
|
|
137
|
-
createReloadResponse,
|
|
138
145
|
createResponseWithMergedHeaders,
|
|
139
146
|
createSimpleRedirectResponse,
|
|
140
|
-
finalizeResponse,
|
|
141
|
-
interceptRedirectForPartial,
|
|
142
|
-
rewrapResponseRouteResponse,
|
|
143
147
|
} from "../rsc/helpers.js";
|
|
144
|
-
import { guardOutgoingRedirect } from "../rsc/redirect-guard.js";
|
|
145
|
-
import { stringifyJsonRouteResult } from "../rsc/json-route-result.js";
|
|
146
|
-
import { isWebSocketUpgradeResponse, trySetHeader } from "../response-utils.js";
|
|
147
|
-
import { invokeOnError } from "../router/error-handling.js";
|
|
148
148
|
import type { OnErrorCallback } from "../types/error-types.js";
|
|
149
149
|
import type { Rango } from "../router/router-interfaces.js";
|
|
150
150
|
import { getRequestId, resolveSink, safeEmit } from "../router/telemetry.js";
|
|
151
151
|
import type { TelemetrySink } from "../router/telemetry.js";
|
|
152
|
-
import {
|
|
153
|
-
RouterTimeoutError,
|
|
154
|
-
createDefaultTimeoutResponse,
|
|
155
|
-
isTimeoutEnabled,
|
|
156
|
-
withTimeout,
|
|
157
|
-
} from "../router/timeout.js";
|
|
158
152
|
import type { OnTimeoutCallback, ResolvedTimeouts } from "../router/timeout.js";
|
|
159
|
-
import {
|
|
153
|
+
import { WIRE } from "../wire-params.js";
|
|
160
154
|
|
|
161
155
|
/**
|
|
162
156
|
* The internal subset of the router surface dispatch depends on. The public
|
|
@@ -181,26 +175,7 @@ interface DispatchableRouter<TEnv> {
|
|
|
181
175
|
telemetry?: TelemetrySink;
|
|
182
176
|
timeouts?: ResolvedTimeouts;
|
|
183
177
|
onTimeout?: OnTimeoutCallback<TEnv>;
|
|
184
|
-
findMatch
|
|
185
|
-
redirectTo?: string;
|
|
186
|
-
routeKey?: string;
|
|
187
|
-
params?: Record<string, string>;
|
|
188
|
-
} | null>;
|
|
189
|
-
previewMatch(
|
|
190
|
-
request: Request,
|
|
191
|
-
input?: { env?: TEnv },
|
|
192
|
-
): Promise<{
|
|
193
|
-
routeMiddleware?: Array<{
|
|
194
|
-
handler: MiddlewareFn<TEnv>;
|
|
195
|
-
params: Record<string, string>;
|
|
196
|
-
}>;
|
|
197
|
-
responseType?: string;
|
|
198
|
-
handler?: Function;
|
|
199
|
-
params?: Record<string, string>;
|
|
200
|
-
routeKey?: string;
|
|
201
|
-
negotiated?: boolean;
|
|
202
|
-
manifestEntry?: EntryData;
|
|
203
|
-
} | null>;
|
|
178
|
+
findMatch: ClassifyRequestDeps<TEnv>["findMatch"];
|
|
204
179
|
basename?: string;
|
|
205
180
|
cache?:
|
|
206
181
|
| {
|
|
@@ -236,153 +211,44 @@ function toRequest(request: Request | string): Request {
|
|
|
236
211
|
return new Request(new URL(request, DEFAULT_ORIGIN));
|
|
237
212
|
}
|
|
238
213
|
|
|
214
|
+
type RequestStages = typeof import("../rsc/request-stages.js");
|
|
215
|
+
|
|
216
|
+
/** The plan modes dispatch answers; the others render a component. */
|
|
217
|
+
const ANSWERS_MODE: Record<RequestPlan["mode"], boolean> = {
|
|
218
|
+
redirect: true,
|
|
219
|
+
response: true,
|
|
220
|
+
"version-mismatch": true,
|
|
221
|
+
"app-switch": true,
|
|
222
|
+
"full-render": false,
|
|
223
|
+
"partial-render": false,
|
|
224
|
+
action: false,
|
|
225
|
+
loader: false,
|
|
226
|
+
"pe-render": false,
|
|
227
|
+
};
|
|
228
|
+
|
|
239
229
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* A handler-returned Response is NOT routed here — callHandler re-wraps it via
|
|
247
|
-
* rewrapHandlerResponse (mirroring handleResponseRoute's rewrapResponse) so the
|
|
248
|
-
* WebSocket-upgrade bypass and Set-Cookie-preserving header merge match
|
|
249
|
-
* production.
|
|
230
|
+
* Classify one pass (the visitor's URL, or a rewrite target) with the
|
|
231
|
+
* handler's classify stage. A plan that renders a component throws: that is
|
|
232
|
+
* a hard boundary of this primitive (no Flight runtime), so it comes before
|
|
233
|
+
* the pass's global middleware, ahead of any middleware side effect. An
|
|
234
|
+
* unmatched path is answered (404).
|
|
250
235
|
*/
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// returns -- so a dispatch json test fails exactly where production would,
|
|
260
|
-
// instead of silently emitting {} and passing.
|
|
261
|
-
return new Response(stringifyJsonRouteResult(result), {
|
|
262
|
-
status: 200,
|
|
263
|
-
headers: { "content-type": "application/json;charset=utf-8" },
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (Object.hasOwn(RESPONSE_TYPE_MIME, responseType)) {
|
|
268
|
-
return new Response(String(result), {
|
|
269
|
-
status: 200,
|
|
270
|
-
headers: {
|
|
271
|
-
"content-type": `${RESPONSE_TYPE_MIME[responseType]};charset=utf-8`,
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
236
|
+
async function classifyPass<TEnv>(
|
|
237
|
+
stages: RequestStages,
|
|
238
|
+
request: Request,
|
|
239
|
+
url: URL,
|
|
240
|
+
ctx: ClassifyContext<TEnv>,
|
|
241
|
+
): Promise<RequestPlan<TEnv>> {
|
|
242
|
+
const plan = await stages.classify(request, url, ctx);
|
|
243
|
+
if (ANSWERS_MODE[plan.mode] || stages.isUnmatchedPlan(plan)) return plan;
|
|
276
244
|
throw new Error(
|
|
277
|
-
`dispatch()
|
|
278
|
-
`
|
|
279
|
-
`
|
|
245
|
+
`dispatch() does not render RSC routes — the route matched at ` +
|
|
246
|
+
`"${url.pathname}" is a React Server Component route, not a response ` +
|
|
247
|
+
`route. Use renderHandler/renderServerTree/renderToFlightString or an ` +
|
|
248
|
+
`e2e test to exercise component rendering.`,
|
|
280
249
|
);
|
|
281
250
|
}
|
|
282
251
|
|
|
283
|
-
/**
|
|
284
|
-
* Serialize a thrown handler error into the same typed Response the router's
|
|
285
|
-
* handleResponseRoute() catch block produces:
|
|
286
|
-
* - "json" routes return an RFC 9457 problem+json body (application/problem+json),
|
|
287
|
-
* - all other types return a text/plain body (the RouterError message verbatim,
|
|
288
|
-
* the Error message in dev, else "Internal Server Error").
|
|
289
|
-
*
|
|
290
|
-
* `status` is the effective HTTP status resolved by the caller (RouterError.status
|
|
291
|
-
* or 500, overridden by ctx.setStatus()); it governs both the HTTP status and the
|
|
292
|
-
* problem body's `status`/`title` members. Reuses the production
|
|
293
|
-
* createProblemDetails so the error body is byte-identical rather than re-derived.
|
|
294
|
-
*/
|
|
295
|
-
function serializeResponseRouteError(
|
|
296
|
-
error: unknown,
|
|
297
|
-
responseType: string,
|
|
298
|
-
status: number,
|
|
299
|
-
): Response {
|
|
300
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
301
|
-
|
|
302
|
-
if (responseType === "json") {
|
|
303
|
-
return new Response(
|
|
304
|
-
JSON.stringify(createProblemDetails(error, status, isDev)),
|
|
305
|
-
{
|
|
306
|
-
status,
|
|
307
|
-
headers: { "content-type": "application/problem+json;charset=utf-8" },
|
|
308
|
-
},
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const message =
|
|
313
|
-
error instanceof RouterError
|
|
314
|
-
? error.message
|
|
315
|
-
: isDev && error instanceof Error
|
|
316
|
-
? error.message
|
|
317
|
-
: "Internal Server Error";
|
|
318
|
-
return new Response(message, {
|
|
319
|
-
status,
|
|
320
|
-
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** What one pass of a dispatch matched (the visitor's URL, or a rewrite target). */
|
|
325
|
-
interface DispatchPass<TEnv> {
|
|
326
|
-
req: Request;
|
|
327
|
-
url: URL;
|
|
328
|
-
redirectTo?: string;
|
|
329
|
-
isUnmatched: boolean;
|
|
330
|
-
preview: Awaited<ReturnType<DispatchableRouter<TEnv>["previewMatch"]>>;
|
|
331
|
-
responseType?: string;
|
|
332
|
-
handler?: Function;
|
|
333
|
-
params: Record<string, string>;
|
|
334
|
-
routeKey?: string;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async function matchDispatchPass<TEnv>(
|
|
338
|
-
router: DispatchableRouter<TEnv>,
|
|
339
|
-
req: Request,
|
|
340
|
-
env: TEnv,
|
|
341
|
-
): Promise<DispatchPass<TEnv>> {
|
|
342
|
-
const url = new URL(req.url);
|
|
343
|
-
// findMatch carries trailing-slash/redirect targets and null on no match.
|
|
344
|
-
// previewMatch swallows redirects, so detect them here first.
|
|
345
|
-
const match = await router.findMatch(url.pathname);
|
|
346
|
-
const redirectTo = match?.redirectTo;
|
|
347
|
-
const isUnmatched = !match;
|
|
348
|
-
|
|
349
|
-
// previewMatch resolves responseType, the response-route handler, and the
|
|
350
|
-
// route middleware from the matched entry tree (with content negotiation).
|
|
351
|
-
// Skip it for a redirect/unmatched path — there is no response route to
|
|
352
|
-
// resolve, and previewMatch would return null / a redirect marker anyway.
|
|
353
|
-
const preview =
|
|
354
|
-
redirectTo || isUnmatched ? null : await router.previewMatch(req, { env });
|
|
355
|
-
|
|
356
|
-
// A bare match with no responseType is an RSC route. The RSC-route throw is a
|
|
357
|
-
// hard boundary of this primitive (no Flight runtime), distinct from the
|
|
358
|
-
// 308/404 outcomes, so it stays a pre-middleware guard.
|
|
359
|
-
const responseType = preview?.responseType;
|
|
360
|
-
const handler = preview?.handler;
|
|
361
|
-
if (
|
|
362
|
-
!redirectTo &&
|
|
363
|
-
!isUnmatched &&
|
|
364
|
-
(!responseType || typeof handler !== "function")
|
|
365
|
-
) {
|
|
366
|
-
throw new Error(
|
|
367
|
-
`dispatch() does not render RSC routes — the route matched at ` +
|
|
368
|
-
`"${url.pathname}" is a React Server Component route, not a response ` +
|
|
369
|
-
`route. Use renderHandler/renderServerTree/renderToFlightString or an ` +
|
|
370
|
-
`e2e test to exercise component rendering.`,
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
return {
|
|
374
|
-
req,
|
|
375
|
-
url,
|
|
376
|
-
redirectTo,
|
|
377
|
-
isUnmatched,
|
|
378
|
-
preview,
|
|
379
|
-
responseType,
|
|
380
|
-
handler,
|
|
381
|
-
params: preview?.params ?? match?.params ?? {},
|
|
382
|
-
routeKey: preview?.routeKey ?? match?.routeKey,
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
|
|
386
252
|
/**
|
|
387
253
|
* Run a request through the router in-process and return the Response.
|
|
388
254
|
*
|
|
@@ -412,16 +278,49 @@ export async function dispatch<TEnv = any>(
|
|
|
412
278
|
const routerId = router.id ?? router.routerId;
|
|
413
279
|
await ensureRouterTrie(router);
|
|
414
280
|
|
|
415
|
-
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
const
|
|
281
|
+
const stages = await loadRequestStages();
|
|
282
|
+
// The store keys with the router's versions, resolved as the production
|
|
283
|
+
// handler resolves them (rsc/handler.ts).
|
|
284
|
+
const versions = resolveRouterVersions(routerId);
|
|
285
|
+
const ctx: RequestStageContext<TEnv> & ClassifyContext<TEnv> = {
|
|
286
|
+
router: {
|
|
287
|
+
id: routerId ?? "",
|
|
288
|
+
findMatch: router.findMatch,
|
|
289
|
+
basename: router.basename,
|
|
290
|
+
middleware: router.middleware,
|
|
291
|
+
onError: router.onError,
|
|
292
|
+
telemetry: router.telemetry,
|
|
293
|
+
timeouts: router.timeouts,
|
|
294
|
+
onTimeout: router.onTimeout,
|
|
295
|
+
},
|
|
296
|
+
callOnError: stages.createErrorReporter<TEnv>(router),
|
|
297
|
+
// A response-route handler's own throw is serialized into the same typed
|
|
298
|
+
// Response as production but not reported (module header).
|
|
299
|
+
reportResponseRouteError: () => {},
|
|
300
|
+
getRequiredRouteMap: () => router.routeMap as Record<string, string>,
|
|
301
|
+
// No Flight runtime: the no-state 204 + X-RSC-Redirect, dropping any
|
|
302
|
+
// location state (module header). `external` is forwarded so
|
|
303
|
+
// redirect(url, { external: true }) keeps its off-host target.
|
|
304
|
+
createRedirectFlightResponse: (redirectUrl, _locationState, external) =>
|
|
305
|
+
createSimpleRedirectResponse(redirectUrl, {
|
|
306
|
+
requestOrigin: url.origin,
|
|
307
|
+
basename: router.basename,
|
|
308
|
+
external,
|
|
309
|
+
}),
|
|
310
|
+
// The `_rsc_v` reload check reads the DOCUMENT version, as the handler's.
|
|
311
|
+
version: versions.document,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// The visitor's URL is classified before any middleware runs, so an RSC
|
|
315
|
+
// route fails here, before a middleware side effect. A rewrite target is
|
|
316
|
+
// classified the same way, at the start of its pass (below).
|
|
317
|
+
let plan = await classifyPass(stages, req, url, ctx);
|
|
419
318
|
|
|
420
319
|
const variables: Record<string, unknown> = {};
|
|
421
320
|
|
|
422
321
|
// Resolve the router's cache store the way the production handler does, so a
|
|
423
322
|
// "use cache" inside a response-route handler reaches the request-scope
|
|
424
|
-
// (NOCACHE) detection
|
|
323
|
+
// (NOCACHE) detection instead of bypassing on a missing store.
|
|
425
324
|
let cacheStore: SegmentCacheStore | undefined;
|
|
426
325
|
let searchParamsFilter: SearchParamsFilter | undefined;
|
|
427
326
|
const cacheOption = router.cache;
|
|
@@ -444,26 +343,11 @@ export async function dispatch<TEnv = any>(
|
|
|
444
343
|
cacheStore,
|
|
445
344
|
searchParamsFilter,
|
|
446
345
|
cacheProfiles: router.cacheProfiles,
|
|
447
|
-
|
|
448
|
-
// handler resolves them (rsc/handler.ts).
|
|
449
|
-
versions: resolveRouterVersions(routerId),
|
|
346
|
+
versions,
|
|
450
347
|
});
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
requestContext._reportBackgroundError = (error, category) => {
|
|
455
|
-
if (error != null && typeof error === "object") {
|
|
456
|
-
if (requestContext._reportedErrors.has(error)) return;
|
|
457
|
-
requestContext._reportedErrors.add(error);
|
|
458
|
-
}
|
|
459
|
-
invokeOnError(
|
|
460
|
-
router.onError,
|
|
461
|
-
error,
|
|
462
|
-
"cache",
|
|
463
|
-
{ request: req, url, metadata: { category } },
|
|
464
|
-
"RSC",
|
|
465
|
-
);
|
|
466
|
-
};
|
|
348
|
+
// Cache degradation (reportCacheError -> _reportBackgroundError) reaches the
|
|
349
|
+
// router's onError, as in the production handler.
|
|
350
|
+
stages.reportBackgroundErrors(requestContext, req, url, ctx.callOnError);
|
|
467
351
|
// Match production: the RSC handler stores the router's basename on the
|
|
468
352
|
// request context (handler.ts), and redirect() prefixes root-relative URLs
|
|
469
353
|
// with it. Mirror it so basename-redirect tests behave as they do in a real
|
|
@@ -471,21 +355,7 @@ export async function dispatch<TEnv = any>(
|
|
|
471
355
|
requestContext._basename = router.basename;
|
|
472
356
|
requestContext._routerId = routerId;
|
|
473
357
|
|
|
474
|
-
|
|
475
|
-
// it from the route map alone (response-route-handler.ts), with NO matched
|
|
476
|
-
// routeKey or params. Passing routeKey/params here would auto-fill params from
|
|
477
|
-
// the matched route, so ctx.reverse("name") could pass in a test while the
|
|
478
|
-
// real handler throws for the missing param.
|
|
479
|
-
const reverse = createReverseFunction(
|
|
480
|
-
router.routeMap as Record<string, string>,
|
|
481
|
-
) as (
|
|
482
|
-
name: string,
|
|
483
|
-
p?: Record<string, string>,
|
|
484
|
-
search?: Record<string, unknown>,
|
|
485
|
-
) => string;
|
|
486
|
-
|
|
487
|
-
const isPartial = url.searchParams.has("_rsc_partial");
|
|
488
|
-
const isAction = url.searchParams.has("_rsc_action");
|
|
358
|
+
const isPartial = url.searchParams.has(WIRE.partial);
|
|
489
359
|
|
|
490
360
|
// Telemetry: mirror the router's match-transaction lifecycle onto the
|
|
491
361
|
// configured sink so a consumer can unit-test createRouter({ telemetry })
|
|
@@ -501,307 +371,6 @@ export async function dispatch<TEnv = any>(
|
|
|
501
371
|
const telemetryStart = sink ? performance.now() : 0;
|
|
502
372
|
|
|
503
373
|
return runWithRequestContext(requestContext, async () => {
|
|
504
|
-
// Set params before middleware/handler run, so global middleware sees
|
|
505
|
-
// ctx.params (production sets them during matching, before middleware).
|
|
506
|
-
// On a redirect/unmatched path there are no route params.
|
|
507
|
-
const recordPassParams = (pass: DispatchPass<TEnv>): void => {
|
|
508
|
-
if (pass.routeKey !== undefined) {
|
|
509
|
-
setRequestContextParams(pass.params, pass.routeKey);
|
|
510
|
-
} else {
|
|
511
|
-
requestContext.params = pass.params;
|
|
512
|
-
}
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
// The response-route handler (with its own route middleware) lives inside
|
|
516
|
-
// coreHandler below, mirroring production where handleResponseRoute is
|
|
517
|
-
// nested inside coreHandler. Built lazily so a redirect/404 path never
|
|
518
|
-
// touches it.
|
|
519
|
-
const callResponseRoute = async (
|
|
520
|
-
pass: DispatchPass<TEnv>,
|
|
521
|
-
): Promise<Response> => {
|
|
522
|
-
const { req, url, preview, params, responseType, handler } = pass;
|
|
523
|
-
// Match production: a partial (client-navigation) request to a response
|
|
524
|
-
// route is short-circuited to X-RSC-Reload (handleResponseRoute), BEFORE
|
|
525
|
-
// route-level middleware runs. Route-level middleware is skipped on a
|
|
526
|
-
// partial, exactly as production skips it.
|
|
527
|
-
const partialFinalHandler = async (): Promise<Response> =>
|
|
528
|
-
createReloadResponse(requestContext, url);
|
|
529
|
-
|
|
530
|
-
const cleanUrl = new URL(req.url);
|
|
531
|
-
for (const key of [...cleanUrl.searchParams.keys()]) {
|
|
532
|
-
if (key.startsWith("_rsc")) cleanUrl.searchParams.delete(key);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Lightweight response-handler context mirroring handleResponseRoute.
|
|
536
|
-
const responseHandlerCtx = {
|
|
537
|
-
request: req,
|
|
538
|
-
params,
|
|
539
|
-
env,
|
|
540
|
-
searchParams: cleanUrl.searchParams,
|
|
541
|
-
url: cleanUrl,
|
|
542
|
-
originalUrl: requestContext.originalUrl,
|
|
543
|
-
pathname: url.pathname,
|
|
544
|
-
reverse,
|
|
545
|
-
get: requestContext.get,
|
|
546
|
-
header: (name: string, value: string) =>
|
|
547
|
-
requestContext.header(name, value),
|
|
548
|
-
waitUntil: requestContext.waitUntil.bind(requestContext),
|
|
549
|
-
executionContext: requestContext.executionContext,
|
|
550
|
-
_responseType: responseType,
|
|
551
|
-
};
|
|
552
|
-
// Brand as request-scoped so a "use cache" inside a response-route handler
|
|
553
|
-
// is detected as a request-scope violation here exactly as in production
|
|
554
|
-
// (response-route-handler.ts brands the same shape).
|
|
555
|
-
defineRewrittenFrom(responseHandlerCtx, requestContext);
|
|
556
|
-
(responseHandlerCtx as Record<symbol, unknown>)[NOCACHE_SYMBOL] = true;
|
|
557
|
-
|
|
558
|
-
const callHandler = async (): Promise<Response> => {
|
|
559
|
-
let merged: Response;
|
|
560
|
-
try {
|
|
561
|
-
let result: unknown;
|
|
562
|
-
try {
|
|
563
|
-
result = await (handler as Function)(responseHandlerCtx);
|
|
564
|
-
} catch (error) {
|
|
565
|
-
if (!(error instanceof Response)) throw error;
|
|
566
|
-
result = error;
|
|
567
|
-
}
|
|
568
|
-
if (result instanceof Response) {
|
|
569
|
-
merged = rewrapResponseRouteResponse(result);
|
|
570
|
-
} else {
|
|
571
|
-
// Route the serialized (json/text/...) body through the SAME
|
|
572
|
-
// production finalizer the RSC handler uses, so ctx.onResponse()
|
|
573
|
-
// callbacks fire and stub headers/cookies + the ctx.setStatus
|
|
574
|
-
// override merge identically to production. Runs inside
|
|
575
|
-
// runWithRequestContext, so _getRequestContext() resolves here.
|
|
576
|
-
const serialized = serializeResponseRouteResult(
|
|
577
|
-
result,
|
|
578
|
-
responseType as string,
|
|
579
|
-
);
|
|
580
|
-
merged = createResponseWithMergedHeaders(serialized.body, {
|
|
581
|
-
status: serialized.status,
|
|
582
|
-
headers: serialized.headers,
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
} catch (error) {
|
|
586
|
-
// Mirror handleResponseRoute's catch: a genuine handler error becomes
|
|
587
|
-
// the router's typed 500 / RouterError-status Response (NOT a rejected
|
|
588
|
-
// promise). Middleware short-circuit via thrown Response is handled by
|
|
589
|
-
// executeMiddleware and never reaches here.
|
|
590
|
-
const derivedStatus =
|
|
591
|
-
error instanceof RouterError ? error.status : 500;
|
|
592
|
-
// Resolve the effective status the way createResponseWithMergedHeaders
|
|
593
|
-
// (below) will (ctx.res.status override) BEFORE building the problem
|
|
594
|
-
// body, so the body's status/title match the actual HTTP status when a
|
|
595
|
-
// handler called ctx.setStatus() before throwing — exactly as
|
|
596
|
-
// handleResponseRoute resolves it.
|
|
597
|
-
const status =
|
|
598
|
-
requestContext.res.status !== 200
|
|
599
|
-
? requestContext.res.status
|
|
600
|
-
: derivedStatus;
|
|
601
|
-
const serialized = serializeResponseRouteError(
|
|
602
|
-
error,
|
|
603
|
-
responseType as string,
|
|
604
|
-
status,
|
|
605
|
-
);
|
|
606
|
-
merged = createResponseWithMergedHeaders(serialized.body, {
|
|
607
|
-
status: serialized.status,
|
|
608
|
-
headers: serialized.headers,
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Append Vary: Accept on content-negotiated responses, matching
|
|
613
|
-
// handleResponseRoute's callHandlerWithVary. Skipped on WebSocket
|
|
614
|
-
// upgrade responses (immutable headers, Vary meaningless for a 101).
|
|
615
|
-
if (preview?.negotiated && !isWebSocketUpgradeResponse(merged)) {
|
|
616
|
-
merged.headers.append("Vary", "Accept");
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
return merged;
|
|
620
|
-
};
|
|
621
|
-
|
|
622
|
-
// On a partial request the reload IS the terminal handler and route
|
|
623
|
-
// middleware is skipped; otherwise the response-route handler is wrapped
|
|
624
|
-
// by route-level middleware (production order: route middleware runs
|
|
625
|
-
// inside handleResponseRoute, after the global chain).
|
|
626
|
-
if (isPartial) {
|
|
627
|
-
return partialFinalHandler();
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// executeHandler = callHandler wrapped by route-level middleware, exactly
|
|
631
|
-
// the unit the production response cache wraps (response-route-handler.ts).
|
|
632
|
-
const executeHandler = (): Promise<Response> => {
|
|
633
|
-
const routeMiddlewareEntries = (preview?.routeMiddleware ?? []).map(
|
|
634
|
-
(mw) => ({
|
|
635
|
-
entry: {
|
|
636
|
-
pattern: null,
|
|
637
|
-
regex: null,
|
|
638
|
-
paramNames: [],
|
|
639
|
-
handler: mw.handler,
|
|
640
|
-
} as MiddlewareEntry<TEnv>,
|
|
641
|
-
params: mw.params,
|
|
642
|
-
}),
|
|
643
|
-
);
|
|
644
|
-
if (routeMiddlewareEntries.length === 0) {
|
|
645
|
-
return callHandler();
|
|
646
|
-
}
|
|
647
|
-
return executeMiddleware<TEnv>(
|
|
648
|
-
routeMiddlewareEntries,
|
|
649
|
-
req,
|
|
650
|
-
env,
|
|
651
|
-
variables,
|
|
652
|
-
callHandler,
|
|
653
|
-
reverse,
|
|
654
|
-
);
|
|
655
|
-
};
|
|
656
|
-
|
|
657
|
-
// Response-route cache path: resolved through the SAME shared serve leaf
|
|
658
|
-
// (rsc/response-cache-serve.ts) production uses, so a cached
|
|
659
|
-
// path.json/path.text route hits/SWRs/writes tags through dispatch exactly
|
|
660
|
-
// as in production — and the two can never drift. Resolved from the matched
|
|
661
|
-
// entry tree (preview.manifestEntry), which previewMatch surfaces for
|
|
662
|
-
// response routes.
|
|
663
|
-
const manifestEntry = preview?.manifestEntry;
|
|
664
|
-
if (manifestEntry) {
|
|
665
|
-
// Lazy so the testing barrel's eager graph stays plugin-rsc-free (see the
|
|
666
|
-
// import note above): the leaf takes createCacheScope as an INJECTED
|
|
667
|
-
// dep so it never imports plugin-rsc; we hand it the lazily imported
|
|
668
|
-
// builder here, only once a response route actually matched.
|
|
669
|
-
const cacheScopeMod = await import("../cache/cache-scope.js");
|
|
670
|
-
const { serveResponseRouteWithCache } =
|
|
671
|
-
await import("../rsc/response-cache-serve.js");
|
|
672
|
-
// requestContext is RequestContext<TEnv>; the leaf is typed against the
|
|
673
|
-
// default-env RequestContext (it reads only env-agnostic config).
|
|
674
|
-
// Assignable in the router's own tsc but not when a consumer pins a
|
|
675
|
-
// concrete Env — cast to the leaf's param type.
|
|
676
|
-
const cached = await serveResponseRouteWithCache({
|
|
677
|
-
reqCtx: requestContext as Parameters<
|
|
678
|
-
typeof serveResponseRouteWithCache
|
|
679
|
-
>[0]["reqCtx"],
|
|
680
|
-
manifestEntry,
|
|
681
|
-
responseType: responseType as string,
|
|
682
|
-
url,
|
|
683
|
-
executeHandler,
|
|
684
|
-
deps: { createCacheScope: cacheScopeMod.createCacheScope },
|
|
685
|
-
});
|
|
686
|
-
if (cached !== undefined) return cached;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
return executeHandler().then(finalizeResponse);
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
// The renderStartMs deadline wraps ONLY the response-route unit, mirroring
|
|
693
|
-
// handler.ts:845 where withTimeout(handleResponseRoute(...)) runs INSIDE
|
|
694
|
-
// coreRequestHandler — after the global middleware chain has already
|
|
695
|
-
// entered. Wrapping the outer chain (as an earlier version did) counted
|
|
696
|
-
// global middleware time toward the deadline, so a slow global auth
|
|
697
|
-
// middleware could 504 a request production serves. On timeout the 504/
|
|
698
|
-
// onTimeout Response is RETURNED from coreHandler so it flows back out
|
|
699
|
-
// through the global chain and finalizeResponse, identical to production
|
|
700
|
-
// (handler.ts:857 returns handleTimeoutResponse into the same next() path).
|
|
701
|
-
const runResponseRoute = async (
|
|
702
|
-
pass: DispatchPass<TEnv>,
|
|
703
|
-
): Promise<Response> => {
|
|
704
|
-
const { routeKey } = pass;
|
|
705
|
-
const responseWork = callResponseRoute(pass);
|
|
706
|
-
const renderOutcome = await withTimeout(
|
|
707
|
-
responseWork,
|
|
708
|
-
router.timeouts?.renderStartMs,
|
|
709
|
-
"render-start",
|
|
710
|
-
);
|
|
711
|
-
if (!renderOutcome.timedOut) return renderOutcome.result;
|
|
712
|
-
|
|
713
|
-
// The race's losing side keeps executing after the timeout wins; its
|
|
714
|
-
// eventual Response is dropped. Swallow any rejection (no
|
|
715
|
-
// unhandled-rejection surfacing) and cancel the orphaned body stream so,
|
|
716
|
-
// under vitest, the abandoned handler cannot leak into the next test.
|
|
717
|
-
// Production abandons it too (handler.ts:845); this is a runner-scoped
|
|
718
|
-
// cleanup, not an observable-behavior change.
|
|
719
|
-
void responseWork
|
|
720
|
-
.then(
|
|
721
|
-
(orphan) => orphan.body?.cancel(),
|
|
722
|
-
() => {},
|
|
723
|
-
)
|
|
724
|
-
.catch(() => {});
|
|
725
|
-
|
|
726
|
-
const durationMs = renderOutcome.durationMs;
|
|
727
|
-
const timeoutError = new RouterTimeoutError("render-start", durationMs);
|
|
728
|
-
// Field-for-field parity with handleTimeoutResponse's callOnError
|
|
729
|
-
// (handler.ts:254): env is passed through (was previously dropped). render
|
|
730
|
-
// is always absent — dispatch runs no Flight/HTML driver, so there is no
|
|
731
|
-
// foreground cursor to snapshot.
|
|
732
|
-
invokeOnError(
|
|
733
|
-
router.onError,
|
|
734
|
-
timeoutError,
|
|
735
|
-
"handler",
|
|
736
|
-
{
|
|
737
|
-
request: req,
|
|
738
|
-
url,
|
|
739
|
-
env,
|
|
740
|
-
routeKey,
|
|
741
|
-
handledByBoundary: false,
|
|
742
|
-
metadata: { timeout: true, phase: "render-start", durationMs },
|
|
743
|
-
},
|
|
744
|
-
"RSC",
|
|
745
|
-
);
|
|
746
|
-
if (sink) {
|
|
747
|
-
safeEmit(resolveSink(sink), {
|
|
748
|
-
type: "request.timeout",
|
|
749
|
-
timestamp: performance.now(),
|
|
750
|
-
requestId: telemetryRequestId,
|
|
751
|
-
phase: "render-start",
|
|
752
|
-
pathname: url.pathname,
|
|
753
|
-
routeKey,
|
|
754
|
-
durationMs,
|
|
755
|
-
customHandler: router.onTimeout !== undefined,
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
if (router.onTimeout) {
|
|
759
|
-
try {
|
|
760
|
-
return await router.onTimeout({
|
|
761
|
-
phase: "render-start",
|
|
762
|
-
request: req,
|
|
763
|
-
url,
|
|
764
|
-
env,
|
|
765
|
-
routeKey,
|
|
766
|
-
durationMs,
|
|
767
|
-
});
|
|
768
|
-
} catch (error) {
|
|
769
|
-
if (process.env.NODE_ENV !== "production") {
|
|
770
|
-
console.error("[dispatch] onTimeout callback error:", error);
|
|
771
|
-
}
|
|
772
|
-
return createDefaultTimeoutResponse("render-start");
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
return createDefaultTimeoutResponse("render-start");
|
|
776
|
-
};
|
|
777
|
-
|
|
778
|
-
// coreHandler is the single terminal the global middleware chain wraps,
|
|
779
|
-
// mirroring production's coreHandler (handler.ts): a trailing-slash/redirect
|
|
780
|
-
// 308, an unmatched-path 404, or the response route. Both the 308 and the
|
|
781
|
-
// 404 are produced via createResponseWithMergedHeaders so middleware-set
|
|
782
|
-
// cookies/headers merge onto them, identical to production's
|
|
783
|
-
// rsc-rendering.ts redirect path — and because they sit inside the chain, a
|
|
784
|
-
// global middleware that short-circuits (e.g. an auth 401) runs first and
|
|
785
|
-
// wins, never reaching the 308/404. The renderStartMs deadline lives inside
|
|
786
|
-
// runResponseRoute (below the chain), never covering the 308/404 paths —
|
|
787
|
-
// production only wraps the response-route plan (handler.ts:834-856).
|
|
788
|
-
const coreHandler = async (pass: DispatchPass<TEnv>): Promise<Response> => {
|
|
789
|
-
const { redirectTo, isUnmatched, url } = pass;
|
|
790
|
-
if (redirectTo) {
|
|
791
|
-
return createResponseWithMergedHeaders(null, {
|
|
792
|
-
status: 308,
|
|
793
|
-
headers: { Location: redirectTo + url.search },
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
if (isUnmatched) {
|
|
797
|
-
return createResponseWithMergedHeaders("Not Found", {
|
|
798
|
-
status: 404,
|
|
799
|
-
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
return runResponseRoute(pass);
|
|
803
|
-
};
|
|
804
|
-
|
|
805
374
|
// request.start opens the match transaction, mirroring match-handlers.ts.
|
|
806
375
|
// transaction is always "match" (dispatch has no matchPartial split);
|
|
807
376
|
// isPartial carries the ?_rsc_partial signal the same way production does.
|
|
@@ -818,168 +387,85 @@ export async function dispatch<TEnv = any>(
|
|
|
818
387
|
}
|
|
819
388
|
|
|
820
389
|
try {
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
390
|
+
// The production handler's global middleware passes, terminal plans,
|
|
391
|
+
// response route and finalization (rsc/request-stages.ts).
|
|
392
|
+
const { response, rewrittenTo, hasMiddleware } =
|
|
393
|
+
await stages.runRequestPasses({
|
|
394
|
+
ctx,
|
|
395
|
+
reqCtx: requestContext,
|
|
396
|
+
request: req,
|
|
397
|
+
url,
|
|
398
|
+
env,
|
|
399
|
+
variables,
|
|
400
|
+
beforePass: async (passRequest, passUrl, index) => {
|
|
401
|
+
if (index > 0) {
|
|
402
|
+
plan = await classifyPass(stages, passRequest, passUrl, ctx);
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
// The core the global chain wraps, as the handler's
|
|
406
|
+
// coreRequestHandler: the terminal plans, then a findMatch()
|
|
407
|
+
// redirect's 308, the response route, or a 404. The 308 and 404 go
|
|
408
|
+
// through createResponseWithMergedHeaders, so middleware
|
|
409
|
+
// cookies/headers merge onto them as production's render path
|
|
410
|
+
// merges them.
|
|
411
|
+
core: async (passRequest, passUrl) => {
|
|
412
|
+
const terminal = stages.terminalPlanResponse(plan, passUrl, ctx);
|
|
413
|
+
if (terminal) return terminal;
|
|
414
|
+
const executable = plan as ExecutableRequestPlan<TEnv>;
|
|
415
|
+
stages.recordPlanRoute(executable);
|
|
416
|
+
if (executable.mode === "redirect") {
|
|
417
|
+
return createResponseWithMergedHeaders(null, {
|
|
418
|
+
status: 308,
|
|
419
|
+
headers: { Location: executable.redirectUrl },
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (executable.mode === "response") {
|
|
423
|
+
return stages.serveResponseRoute(ctx, {
|
|
424
|
+
match: stages.responseRouteMatch(executable),
|
|
425
|
+
routeKey: executable.route.routeKey,
|
|
426
|
+
request: passRequest,
|
|
427
|
+
url: passUrl,
|
|
854
428
|
env,
|
|
855
429
|
variables,
|
|
856
|
-
|
|
857
|
-
reverse,
|
|
858
|
-
);
|
|
859
|
-
},
|
|
860
|
-
report: (error, at) => {
|
|
861
|
-
invokeOnError(
|
|
862
|
-
router.onError,
|
|
863
|
-
error,
|
|
864
|
-
"routing",
|
|
865
|
-
{ request: at.request, url: at.url, env, handledByBoundary: false },
|
|
866
|
-
"RSC",
|
|
867
|
-
);
|
|
868
|
-
},
|
|
869
|
-
});
|
|
870
|
-
|
|
871
|
-
// Match production's global-chain exit (handler.ts): on a partial/action
|
|
872
|
-
// request a middleware 3xx redirect is converted to a Flight-safe response
|
|
873
|
-
// so fetch() does not auto-follow it; every path then drains onResponse
|
|
874
|
-
// callbacks via finalizeResponse. dispatch is RSC-free, so the
|
|
875
|
-
// createRedirectFlightResponse stand-in falls back to the no-state
|
|
876
|
-
// 204 + X-RSC-Redirect (see the location-state divergence in the header).
|
|
877
|
-
let finalResponse: Response;
|
|
878
|
-
if (isPartial || isAction) {
|
|
879
|
-
const softOpts = {
|
|
880
|
-
requestOrigin: url.origin,
|
|
881
|
-
basename: router.basename,
|
|
882
|
-
};
|
|
883
|
-
// Pass `external` through: interceptRedirectForPartial forwards the
|
|
884
|
-
// out-of-band brand as the third callback arg (helpers.ts). Dropping it
|
|
885
|
-
// would neutralize redirect(url, { external: true }) to "/" in dispatch
|
|
886
|
-
// while production preserves it via createRedirectFlightResponse.
|
|
887
|
-
const intercepted = interceptRedirectForPartial(
|
|
888
|
-
mwResponse,
|
|
889
|
-
(redirectUrl, _locationState, external) =>
|
|
890
|
-
createSimpleRedirectResponse(redirectUrl, {
|
|
891
|
-
...softOpts,
|
|
892
|
-
external,
|
|
893
|
-
}),
|
|
894
|
-
softOpts,
|
|
895
|
-
);
|
|
896
|
-
finalResponse = finalizeResponse(intercepted ?? mwResponse);
|
|
897
|
-
} else {
|
|
898
|
-
finalResponse = finalizeResponse(mwResponse);
|
|
899
|
-
}
|
|
900
|
-
if (rewrittenTo !== undefined) {
|
|
901
|
-
trySetHeader(finalResponse, REWRITE_HEADER, rewrittenTo);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// request.end closes the transaction. dispatch produces no RSC segments and
|
|
905
|
-
// holds no match-cache state, so segmentCount/cacheHit are 0/false — the
|
|
906
|
-
// same shape production emits for its own redirect (segment-less) result.
|
|
907
|
-
if (sink) {
|
|
908
|
-
safeEmit(resolveSink(sink), {
|
|
909
|
-
type: "request.end",
|
|
910
|
-
timestamp: performance.now(),
|
|
911
|
-
requestId: telemetryRequestId,
|
|
912
|
-
method: req.method,
|
|
913
|
-
pathname: url.pathname,
|
|
914
|
-
transaction: "match",
|
|
915
|
-
durationMs: performance.now() - telemetryStart,
|
|
916
|
-
segmentCount: 0,
|
|
917
|
-
cacheHit: false,
|
|
918
|
-
// dispatch's final response IS built before request.end (unlike
|
|
919
|
-
// match()/matchPartial(), whose Response is built after), so stamp its
|
|
920
|
-
// status — the same field a thrown-Response short-circuit carries.
|
|
921
|
-
status: finalResponse.status,
|
|
922
|
-
});
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// Mirror production's single open-redirect chokepoint (handler.ts): every
|
|
926
|
-
// browser-followed (3xx + Location) redirect is same-origin guarded before
|
|
927
|
-
// it leaves -- a cross-origin Location is rewritten to the basename root
|
|
928
|
-
// unless redirect(url, { external: true }) opted out. Soft partial/action
|
|
929
|
-
// redirects are already resolved at createSimpleRedirectResponse time.
|
|
930
|
-
const guardedResponse = guardOutgoingRedirect(
|
|
931
|
-
finalResponse,
|
|
932
|
-
url.origin,
|
|
933
|
-
router.basename,
|
|
934
|
-
);
|
|
935
|
-
|
|
936
|
-
// Mirror production's stream-idle watchdog (handler.ts response tail),
|
|
937
|
-
// same gating and reporting — dispatch is the userland dogfood surface
|
|
938
|
-
// for timeouts.streamIdleMs, so the primitive carries the real wiring,
|
|
939
|
-
// not a stub. Websocket check FIRST (an upgrade response's body getter
|
|
940
|
-
// must never be poked); onTimeout does not apply mid-stream.
|
|
941
|
-
const streamIdleMs = router.timeouts?.streamIdleMs;
|
|
942
|
-
if (
|
|
943
|
-
isTimeoutEnabled(streamIdleMs) &&
|
|
944
|
-
!isWebSocketUpgradeResponse(guardedResponse) &&
|
|
945
|
-
guardedResponse.body
|
|
946
|
-
) {
|
|
947
|
-
return applyStreamIdleTimeout(
|
|
948
|
-
guardedResponse,
|
|
949
|
-
streamIdleMs!,
|
|
950
|
-
(trip) => {
|
|
951
|
-
invokeOnError(
|
|
952
|
-
router.onError,
|
|
953
|
-
trip.error,
|
|
954
|
-
"handler",
|
|
955
|
-
{
|
|
956
|
-
request: req,
|
|
957
|
-
url,
|
|
958
|
-
env,
|
|
959
|
-
handledByBoundary: false,
|
|
960
|
-
metadata: {
|
|
961
|
-
timeout: true,
|
|
962
|
-
phase: "stream-idle",
|
|
963
|
-
durationMs: trip.totalMs,
|
|
964
|
-
},
|
|
965
|
-
},
|
|
966
|
-
"RSC",
|
|
967
|
-
);
|
|
968
|
-
if (sink) {
|
|
969
|
-
safeEmit(resolveSink(sink), {
|
|
970
|
-
type: "request.timeout",
|
|
971
|
-
timestamp: performance.now(),
|
|
972
|
-
requestId: telemetryRequestId,
|
|
973
|
-
phase: "stream-idle",
|
|
974
|
-
pathname: url.pathname,
|
|
975
|
-
durationMs: trip.totalMs,
|
|
976
|
-
customHandler: false,
|
|
430
|
+
abandon: abandonTimedOut,
|
|
977
431
|
});
|
|
978
432
|
}
|
|
433
|
+
return createResponseWithMergedHeaders("Not Found", {
|
|
434
|
+
status: 404,
|
|
435
|
+
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
436
|
+
});
|
|
979
437
|
},
|
|
980
|
-
);
|
|
981
|
-
|
|
982
|
-
return
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
return stages.finalizeRequestResponse(response, {
|
|
441
|
+
ctx,
|
|
442
|
+
reqCtx: requestContext,
|
|
443
|
+
request: req,
|
|
444
|
+
url,
|
|
445
|
+
env,
|
|
446
|
+
hasMiddleware,
|
|
447
|
+
rewrittenTo,
|
|
448
|
+
// request.end closes the transaction. dispatch produces no RSC
|
|
449
|
+
// segments and holds no match-cache state, so segmentCount/cacheHit
|
|
450
|
+
// are 0/false — the same shape production emits for its own redirect
|
|
451
|
+
// (segment-less) result. The response is built before request.end
|
|
452
|
+
// (unlike match()/matchPartial()), so it carries the status.
|
|
453
|
+
beforeGuard: (finalResponse) => {
|
|
454
|
+
if (!sink) return;
|
|
455
|
+
safeEmit(resolveSink(sink), {
|
|
456
|
+
type: "request.end",
|
|
457
|
+
timestamp: performance.now(),
|
|
458
|
+
requestId: telemetryRequestId,
|
|
459
|
+
method: req.method,
|
|
460
|
+
pathname: url.pathname,
|
|
461
|
+
transaction: "match",
|
|
462
|
+
durationMs: performance.now() - telemetryStart,
|
|
463
|
+
segmentCount: 0,
|
|
464
|
+
cacheHit: false,
|
|
465
|
+
status: finalResponse.status,
|
|
466
|
+
});
|
|
467
|
+
},
|
|
468
|
+
});
|
|
983
469
|
} catch (error) {
|
|
984
470
|
if (sink) {
|
|
985
471
|
if (error instanceof Response) {
|
|
@@ -1020,3 +506,30 @@ export async function dispatch<TEnv = any>(
|
|
|
1020
506
|
}
|
|
1021
507
|
});
|
|
1022
508
|
}
|
|
509
|
+
|
|
510
|
+
let requestStages: Promise<RequestStages> | undefined;
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* The request stages, loaded on the first dispatch so the testing barrel's
|
|
514
|
+
* eager graph stays what a suite that never dispatches loads. Neither graph
|
|
515
|
+
* loads @vitejs/plugin-rsc (__tests__/testing-module-graph.test.ts). Held
|
|
516
|
+
* after the first load: a dynamic import per dispatch costs a module-runner
|
|
517
|
+
* lookup.
|
|
518
|
+
*/
|
|
519
|
+
function loadRequestStages(): Promise<RequestStages> {
|
|
520
|
+
return (requestStages ??= import("../rsc/request-stages.js"));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* A timed-out response route's work keeps running. Swallow its rejection and
|
|
525
|
+
* cancel its body so, under a test runner, the abandoned handler cannot leak
|
|
526
|
+
* into the next test. The production handler leaves it running.
|
|
527
|
+
*/
|
|
528
|
+
function abandonTimedOut(work: Promise<Response>): void {
|
|
529
|
+
void work
|
|
530
|
+
.then(
|
|
531
|
+
(orphan) => orphan.body?.cancel(),
|
|
532
|
+
() => {},
|
|
533
|
+
)
|
|
534
|
+
.catch(() => {});
|
|
535
|
+
}
|