@nifrajs/core 4.0.2 → 5.1.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/README.md +6 -2
- package/dist/assurance.d.ts +86 -6
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +186 -95
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +8 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +53 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/durable-effect.js +1 -1
- package/dist/durable-effect.js.map +1 -1
- package/dist/durable-saga.d.ts +6 -0
- package/dist/durable-saga.d.ts.map +1 -1
- package/dist/durable-saga.js +83 -10
- package/dist/durable-saga.js.map +1 -1
- package/dist/edge-kit.d.ts +8 -3
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +7 -2
- package/dist/edge-kit.js.map +1 -1
- package/dist/effect-scope.d.ts.map +1 -1
- package/dist/effect-scope.js +16 -5
- package/dist/effect-scope.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +9 -1
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/durable-shared.d.ts +1 -2
- package/dist/internal/durable-shared.d.ts.map +1 -1
- package/dist/internal/durable-shared.js +25 -44
- package/dist/internal/durable-shared.js.map +1 -1
- package/dist/internal/durable-types.d.ts +3 -0
- package/dist/internal/durable-types.d.ts.map +1 -1
- package/dist/internal/hook-origins.d.ts +67 -0
- package/dist/internal/hook-origins.d.ts.map +1 -0
- package/dist/internal/hook-origins.js +86 -0
- package/dist/internal/hook-origins.js.map +1 -0
- package/dist/internal/route-assurance.d.ts +6 -3
- package/dist/internal/route-assurance.d.ts.map +1 -1
- package/dist/internal/route-assurance.js +29 -14
- package/dist/internal/route-assurance.js.map +1 -1
- package/dist/internal/route-compiler.d.ts +9 -0
- package/dist/internal/route-compiler.d.ts.map +1 -1
- package/dist/internal/route-compiler.js +95 -13
- package/dist/internal/route-compiler.js.map +1 -1
- package/dist/internal/route-program.d.ts +1 -1
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +59 -16
- package/dist/internal/route-program.js.map +1 -1
- package/dist/internal/same-origin.d.ts +11 -1
- package/dist/internal/same-origin.d.ts.map +1 -1
- package/dist/internal/same-origin.js +8 -0
- package/dist/internal/same-origin.js.map +1 -1
- package/dist/reconciliation-worker.d.ts.map +1 -1
- package/dist/reconciliation-worker.js +12 -2
- package/dist/reconciliation-worker.js.map +1 -1
- package/dist/reflection.d.ts +53 -2
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +352 -5
- package/dist/reflection.js.map +1 -1
- package/dist/router/pattern.d.ts +1 -1
- package/dist/router/pattern.d.ts.map +1 -1
- package/dist/router/pattern.js +6 -3
- package/dist/router/pattern.js.map +1 -1
- package/dist/security/baseline.d.ts +3 -0
- package/dist/security/baseline.d.ts.map +1 -1
- package/dist/security/baseline.js +9 -5
- package/dist/security/baseline.js.map +1 -1
- package/dist/server/auth.d.ts +32 -3
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +23 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/body-lane.d.ts +8 -2
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +27 -2
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +34 -1
- package/dist/server/body.js.map +1 -1
- package/dist/server/client-ip.d.ts +3 -0
- package/dist/server/client-ip.d.ts.map +1 -1
- package/dist/server/client-ip.js +6 -5
- package/dist/server/client-ip.js.map +1 -1
- package/dist/server/context.d.ts +2 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +13 -1
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/edge.d.ts.map +1 -1
- package/dist/server/edge.js +2 -1
- package/dist/server/edge.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +5 -1
- package/dist/server/http.js.map +1 -1
- package/dist/server/idempotency-lane.d.ts +6 -3
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +38 -17
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/install.d.ts +2 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +2 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logger.js +3 -1
- package/dist/server/logger.js.map +1 -1
- package/dist/server/methods.d.ts +1 -0
- package/dist/server/methods.d.ts.map +1 -1
- package/dist/server/methods.js +1 -0
- package/dist/server/methods.js.map +1 -1
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +29 -11
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/platform-adapters.d.ts.map +1 -1
- package/dist/server/platform-adapters.js +3 -2
- package/dist/server/platform-adapters.js.map +1 -1
- package/dist/server/request-context.d.ts +2 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +4 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +44 -11
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +192 -79
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +21 -6
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +139 -14
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts.map +1 -1
- package/dist/server/response-observer-runtime.js +10 -41
- package/dist/server/response-observer-runtime.js.map +1 -1
- package/dist/server/runtime-core.d.ts +2 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +2 -0
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +45 -10
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +67 -32
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +481 -334
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +11 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +18 -3
- package/dist/server/sse.js.map +1 -1
- package/dist/server/transport-lane.d.ts +7 -1
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/webhook.d.ts +5 -2
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +17 -1
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +33 -14
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +47 -12
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +7 -1
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +42 -2
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +15 -3
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +4 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts +6 -0
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +55 -6
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +71 -40
- package/dist/tool-contract.js.map +1 -1
- package/dist/wire.d.ts +5 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +5 -0
- package/dist/wire.js.map +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +4 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/assurance.ts +299 -111
- package/src/capabilities.ts +62 -1
- package/src/durable-effect.ts +1 -1
- package/src/durable-saga.ts +94 -10
- package/src/edge-kit.ts +8 -3
- package/src/effect-scope.ts +15 -5
- package/src/errors.ts +1 -0
- package/src/evidence.ts +12 -3
- package/src/index.ts +7 -2
- package/src/internal/durable-shared.ts +25 -37
- package/src/internal/durable-types.ts +3 -0
- package/src/internal/hook-origins.ts +153 -0
- package/src/internal/route-assurance.ts +32 -18
- package/src/internal/route-compiler.ts +107 -16
- package/src/internal/route-program.ts +98 -44
- package/src/internal/same-origin.ts +14 -1
- package/src/reconciliation-worker.ts +10 -2
- package/src/reflection.ts +455 -4
- package/src/router/pattern.ts +6 -3
- package/src/security/baseline.ts +10 -5
- package/src/server/auth.ts +45 -3
- package/src/server/body-lane.ts +36 -1
- package/src/server/body.ts +34 -1
- package/src/server/client-ip.ts +8 -5
- package/src/server/context.ts +2 -1
- package/src/server/cookies.ts +12 -1
- package/src/server/edge.ts +2 -1
- package/src/server/http.ts +7 -1
- package/src/server/idempotency-lane.ts +64 -22
- package/src/server/install.ts +3 -0
- package/src/server/logger.ts +3 -1
- package/src/server/methods.ts +1 -0
- package/src/server/node-outcome.ts +34 -12
- package/src/server/platform-adapters.ts +3 -2
- package/src/server/request-context.ts +5 -0
- package/src/server/respond.ts +222 -69
- package/src/server/response-contract-lane.ts +162 -10
- package/src/server/response-observer-runtime.ts +16 -38
- package/src/server/runtime-core.ts +2 -0
- package/src/server/server-types.ts +45 -10
- package/src/server/server.ts +707 -539
- package/src/server/sse.ts +84 -55
- package/src/server/transport-lane.ts +7 -1
- package/src/server/webhook.ts +22 -3
- package/src/server/websocket.ts +83 -18
- package/src/server/ws-bun.ts +48 -1
- package/src/server/ws-hook.ts +14 -2
- package/src/server.ts +6 -1
- package/src/single-copy.ts +55 -5
- package/src/tool-contract.ts +67 -39
- package/src/wire.ts +14 -0
- package/src/ws.ts +6 -0
package/src/server/server.ts
CHANGED
|
@@ -17,13 +17,22 @@ import {
|
|
|
17
17
|
DEFAULT_CAPABILITY_INTERCEPTOR_TIMEOUT_MS,
|
|
18
18
|
type RegisteredCapabilityInterceptor,
|
|
19
19
|
} from "../internal/capability-runtime.ts"
|
|
20
|
+
import {
|
|
21
|
+
adoptStaticTags,
|
|
22
|
+
enterPlugin,
|
|
23
|
+
leavePlugin,
|
|
24
|
+
tagAdoption,
|
|
25
|
+
tagHook,
|
|
26
|
+
tagScoped,
|
|
27
|
+
tagStatic,
|
|
28
|
+
} from "../internal/hook-origins.ts"
|
|
20
29
|
import {
|
|
21
30
|
type AssuranceDeclaration,
|
|
22
31
|
assuranceDeclarationsOf,
|
|
23
32
|
assuranceEvidenceFor,
|
|
24
33
|
} from "../internal/route-assurance.ts"
|
|
25
34
|
import { type CatalogRoute, RouteCatalog } from "../internal/route-catalog.ts"
|
|
26
|
-
import { compileRouteOptions } from "../internal/route-compiler.ts"
|
|
35
|
+
import { adoptRouteRuntimes, compileRouteOptions } from "../internal/route-compiler.ts"
|
|
27
36
|
import {
|
|
28
37
|
compileRouteExecutionPlan,
|
|
29
38
|
type FusedBodyRunner,
|
|
@@ -65,6 +74,7 @@ import {
|
|
|
65
74
|
} from "./client-ip.ts"
|
|
66
75
|
import type { Context, Platform, ResponseControls, RouteSchema } from "./context.ts"
|
|
67
76
|
import "./core-copies.ts"
|
|
77
|
+
import type { FormOrigins } from "./body-lane.ts"
|
|
68
78
|
import {
|
|
69
79
|
hasLowercaseHeaderKeysMark,
|
|
70
80
|
headerKeysAllLowercase,
|
|
@@ -107,6 +117,7 @@ import {
|
|
|
107
117
|
fusedRespond,
|
|
108
118
|
fusedRespondNoSet,
|
|
109
119
|
toResponse,
|
|
120
|
+
writableResponse,
|
|
110
121
|
} from "./respond.ts"
|
|
111
122
|
// Type-only: erased, so the kernel never pulls the lane's implementation into a bundle that does not
|
|
112
123
|
// install the plugin. The value side arrives through the symbol-keyed install seam.
|
|
@@ -116,6 +127,7 @@ import type {
|
|
|
116
127
|
ResponseObserverRuntime,
|
|
117
128
|
} from "./response-observer-runtime.ts"
|
|
118
129
|
import {
|
|
130
|
+
CONTEXT_REQUEST,
|
|
119
131
|
CONTEXT_SEARCH,
|
|
120
132
|
CONTEXT_SET,
|
|
121
133
|
EMPTY_RESPONSE_CONTROLS,
|
|
@@ -160,6 +172,7 @@ import {
|
|
|
160
172
|
INSTALL_SSE,
|
|
161
173
|
INSTALL_WS,
|
|
162
174
|
NODE_NATIVE_MOUNT,
|
|
175
|
+
REPORT_ERROR,
|
|
163
176
|
RESOLVE_NODE_MOUNT,
|
|
164
177
|
} from "./install.ts"
|
|
165
178
|
import type { EffectLedgerRuntime } from "./ledger-lane.ts"
|
|
@@ -186,6 +199,7 @@ import type {
|
|
|
186
199
|
import type {
|
|
187
200
|
AdmissionController,
|
|
188
201
|
AdmissionDecision,
|
|
202
|
+
DeclaredMountRoute,
|
|
189
203
|
FetchHandler,
|
|
190
204
|
ListenTlsOptions,
|
|
191
205
|
McpPromptDescriptor,
|
|
@@ -194,6 +208,7 @@ import type {
|
|
|
194
208
|
MountableApp,
|
|
195
209
|
MountFetchOptions,
|
|
196
210
|
MountOptions,
|
|
211
|
+
OpaqueMountDeclaration,
|
|
197
212
|
PromptArgument,
|
|
198
213
|
PromptMessage,
|
|
199
214
|
ResponseFinalization,
|
|
@@ -205,12 +220,7 @@ import type {
|
|
|
205
220
|
} from "./server-types.ts"
|
|
206
221
|
import type { SSEInit, TypedSSEStream } from "./sse.ts"
|
|
207
222
|
import type { SseRuntime } from "./sse-hook.ts"
|
|
208
|
-
import type {
|
|
209
|
-
TopicRegistry,
|
|
210
|
-
WebSocketContext,
|
|
211
|
-
WebSocketHandler,
|
|
212
|
-
WebSocketUpgradeOutcome,
|
|
213
|
-
} from "./websocket.ts"
|
|
223
|
+
import type { TopicRegistry, WebSocketHandler, WebSocketUpgradeOutcome } from "./websocket.ts"
|
|
214
224
|
import type { BunWsData } from "./ws-bun.ts"
|
|
215
225
|
import type { WsRuntime } from "./ws-hook.ts"
|
|
216
226
|
|
|
@@ -285,6 +295,7 @@ export interface RawContext {
|
|
|
285
295
|
readonly jsonReply?: Response | undefined
|
|
286
296
|
readonly jsonBody?: unknown
|
|
287
297
|
readonly [CONTEXT_SEARCH]: string
|
|
298
|
+
readonly [CONTEXT_REQUEST]: Request
|
|
288
299
|
readonly signal: AbortSignal
|
|
289
300
|
readonly budget: RequestBudget
|
|
290
301
|
readonly env: unknown
|
|
@@ -297,14 +308,18 @@ export interface RawContext {
|
|
|
297
308
|
/** Broad shape so the implementation signature is compatible with both typed overloads. */
|
|
298
309
|
type ErasedHandler = (ctx: never) => MaybePromise<HandlerResult>
|
|
299
310
|
|
|
300
|
-
|
|
311
|
+
/** What an `onRequest` hook returns: a reply (a `Response`, or a `status()`/`json()` result that takes
|
|
312
|
+
* the normal response path), a replacement `Request`, or `undefined` to continue. */
|
|
313
|
+
export type OnRequestResult = Response | ResponseResult | Request | undefined
|
|
301
314
|
type RawOnRequest = (req: Request, platform?: Platform) => MaybePromise<OnRequestResult>
|
|
302
315
|
type RawOnResponse = (response: Response, req: Request) => MaybePromise<Response>
|
|
303
316
|
type RawOnResponseFinalized = (outcome: ResponseFinalization, req: Request) => MaybePromise<void>
|
|
304
317
|
|
|
305
|
-
/** A registered WebSocket route
|
|
318
|
+
/** A registered WebSocket route and the route chain its handshake runs; matching reuses
|
|
319
|
+
* {@link Router} under the GET verb. */
|
|
306
320
|
interface WsEntry {
|
|
307
321
|
readonly handler: WebSocketHandler
|
|
322
|
+
readonly chain: RouteEntry
|
|
308
323
|
}
|
|
309
324
|
|
|
310
325
|
/** Structural view of the socket-peer lookup on the Bun `Server`, so any Bun `Server` (WS or not)
|
|
@@ -353,10 +368,14 @@ interface FetchMount<Env = unknown> {
|
|
|
353
368
|
readonly fallbackOn404: boolean
|
|
354
369
|
readonly beforeRoutes: boolean
|
|
355
370
|
readonly order: number
|
|
356
|
-
/**
|
|
357
|
-
readonly opaque: string | undefined
|
|
371
|
+
/** Why this child is not analyzed (`MountOptions.opaque`), checked by reflection. */
|
|
372
|
+
readonly opaque: string | OpaqueMountDeclaration | undefined
|
|
358
373
|
/** The mounted app (`mount()` only), for reflection to tell a composed child from an opaque one. */
|
|
359
374
|
readonly app?: object
|
|
375
|
+
/** Parent request → the stripped copy a handshake check handed this child. On a pass the HTTP
|
|
376
|
+
* dispatch hands over that same copy, which the child's own pass record knows, so its onRequest
|
|
377
|
+
* hooks do not run a second time. */
|
|
378
|
+
wsPassed?: WeakMap<Request, Request>
|
|
360
379
|
}
|
|
361
380
|
|
|
362
381
|
/** The socket peer Bun observed, as a `Platform` for the request lifecycle (`undefined` if unknown). */
|
|
@@ -410,6 +429,7 @@ function outranks(
|
|
|
410
429
|
}
|
|
411
430
|
|
|
412
431
|
const WS_PASS: WebSocketUpgradeOutcome = { kind: "pass" }
|
|
432
|
+
|
|
413
433
|
const DEFAULT_WS_UPGRADE_TIMEOUT_MS = 10_000
|
|
414
434
|
/** Counts `ws()` routes and `mount()`s across every app in this copy of core: the only
|
|
415
435
|
* changes that can make a mounted app take WebSocket upgrades. An app's answer to that question
|
|
@@ -427,6 +447,9 @@ function requireWsRuntime(runtime: WsRuntime | undefined): WsRuntime {
|
|
|
427
447
|
return runtime
|
|
428
448
|
}
|
|
429
449
|
|
|
450
|
+
/** The method `app.sse()` registers: POST when the schema declares a body, which a GET cannot carry. */
|
|
451
|
+
type SseMethodOf<S> = S extends { readonly body: StandardSchemaV1 } ? "POST" : "GET"
|
|
452
|
+
|
|
430
453
|
function requireSseRuntime(runtime: SseRuntime | undefined): SseRuntime {
|
|
431
454
|
if (runtime === undefined) {
|
|
432
455
|
throw new FrameworkError(
|
|
@@ -495,6 +518,7 @@ export type Handler<
|
|
|
495
518
|
export type {
|
|
496
519
|
AdmissionController,
|
|
497
520
|
AdmissionDecision,
|
|
521
|
+
DeclaredMountRoute,
|
|
498
522
|
FetchHandler,
|
|
499
523
|
ListenTlsOptions,
|
|
500
524
|
McpPromptDescriptor,
|
|
@@ -503,6 +527,7 @@ export type {
|
|
|
503
527
|
MountableApp,
|
|
504
528
|
MountFetchOptions,
|
|
505
529
|
MountOptions,
|
|
530
|
+
OpaqueMountDeclaration,
|
|
506
531
|
PromptArgument,
|
|
507
532
|
PromptMessage,
|
|
508
533
|
ResponseFinalization,
|
|
@@ -675,11 +700,16 @@ const underPrefix = (prefix: string): HookScope => {
|
|
|
675
700
|
}
|
|
676
701
|
|
|
677
702
|
/** A merged server's hook scope: exactly the routes MERGED, snapshotted into a dedicated matcher so
|
|
678
|
-
* the guard never reflects whatever the source's own catalog grows into afterwards.
|
|
703
|
+
* the guard never reflects whatever the source's own catalog grows into afterwards. An `OPTIONS`
|
|
704
|
+
* request to a path they serve is in scope too: a CORS preflight names the path, not a route, and
|
|
705
|
+
* the group's `cors()` is the only hook that can answer it. */
|
|
679
706
|
const servedBy = (routes: readonly CatalogRoute[]): HookScope => {
|
|
680
707
|
const scope = new RouteCatalog()
|
|
681
708
|
scope.addBatch(routes)
|
|
682
|
-
return (req) =>
|
|
709
|
+
return (req) => {
|
|
710
|
+
const match = scope.find(req.method, pathnameOf(req.url))
|
|
711
|
+
return match.found || (req.method === "OPTIONS" && match.reason === "method-not-allowed")
|
|
712
|
+
}
|
|
683
713
|
}
|
|
684
714
|
|
|
685
715
|
type AdoptedHook = ((first: never, second: never) => unknown) | undefined
|
|
@@ -800,6 +830,30 @@ interface HookAuditState {
|
|
|
800
830
|
}
|
|
801
831
|
const hookAudit = /* @__PURE__ */ new WeakMap<object, HookAuditState>()
|
|
802
832
|
|
|
833
|
+
/** Whether `from` reaches `target` through mounts that keep the path and take a request under `path`.
|
|
834
|
+
* Module-level, not a `Server` method, so a production bundle drops it with its dev-only caller. */
|
|
835
|
+
function mountLeadsBack(
|
|
836
|
+
from: unknown,
|
|
837
|
+
target: unknown,
|
|
838
|
+
path: string,
|
|
839
|
+
mountsOf: (server: Server) => readonly FetchMount[],
|
|
840
|
+
seen: Set<unknown>,
|
|
841
|
+
): boolean {
|
|
842
|
+
if (from === target) return true
|
|
843
|
+
if (!(from instanceof Server) || seen.has(from)) return false
|
|
844
|
+
seen.add(from)
|
|
845
|
+
const under = (prefix: string): string => (prefix === "/" ? "/" : `${prefix}/`)
|
|
846
|
+
return mountsOf(from).some(
|
|
847
|
+
(mount) =>
|
|
848
|
+
!mount.stripPrefix &&
|
|
849
|
+
(under(path).startsWith(under(mount.path)) || under(mount.path).startsWith(under(path))) &&
|
|
850
|
+
mountLeadsBack(mount.app, target, path, mountsOf, seen),
|
|
851
|
+
)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** Set by `@nifrajs/web`'s `createWebApp` on its app once the page routes are declared. */
|
|
855
|
+
const PAGES_DECLARED = /* @__PURE__ */ Symbol.for("nifra.web.pagesDeclared")
|
|
856
|
+
|
|
803
857
|
/** Start tracking one server's order-scoped hook pushes (once, from the constructor). */
|
|
804
858
|
const beginHookAudit = (
|
|
805
859
|
key: object,
|
|
@@ -810,13 +864,42 @@ const beginHookAudit = (
|
|
|
810
864
|
}
|
|
811
865
|
|
|
812
866
|
/** Record one order-scoped hook push: the route count at that moment (so `routesAtPush === total` at
|
|
813
|
-
* seal means no route was declared after it) and the caller's frame.
|
|
867
|
+
* seal means no route was declared after it) and the caller's frame. Hooks a WebSocket handshake runs
|
|
868
|
+
* count sockets as routes; `around` and `afterHandle` wrap a response, so they count HTTP routes only. */
|
|
814
869
|
const recordScopedHook = (key: object, routesAtPush: number, kind: string): void => {
|
|
870
|
+
// Not a dead hook when routes follow it, but every page already copied its chain without it.
|
|
871
|
+
if (PAGES_DECLARED in key) {
|
|
872
|
+
const site = captureCallerSite()
|
|
873
|
+
throw new FrameworkError(
|
|
874
|
+
"HOOK_AFTER_PAGES",
|
|
875
|
+
`${kind}()${site === undefined ? "" : ` at ${site}`} was added after createWebApp declared the page routes, so no page runs it: each route keeps the order-scoped hooks that existed when it was declared. Apply it in createWebApp({ use: (app) => ... }) to cover the pages, or inside app.group(prefix, ...) to scope it to the routes declared there.`,
|
|
876
|
+
)
|
|
877
|
+
}
|
|
815
878
|
const state = hookAudit.get(key)
|
|
816
879
|
if (state === undefined || state.policy === "off") return
|
|
817
880
|
state.sites.push({ kind, routesAtPush, site: captureCallerSite() })
|
|
818
881
|
}
|
|
819
882
|
|
|
883
|
+
/** Hook provenance for an adoption (dev only, see `internal/hook-origins.ts`): which scope each
|
|
884
|
+
* wrapper just appended to a hook list gates its hook by. `pairs` holds each source list with the target list it
|
|
885
|
+
* was appended to, read right after the append so the wrappers are the target list's tail. */
|
|
886
|
+
const tagAdopted = (
|
|
887
|
+
target: object,
|
|
888
|
+
source: object,
|
|
889
|
+
prefix: string | undefined,
|
|
890
|
+
routes: readonly CatalogRoute[],
|
|
891
|
+
pairs: readonly (readonly [readonly unknown[], readonly unknown[]])[],
|
|
892
|
+
): void => {
|
|
893
|
+
tagAdoption(target, source, prefix)
|
|
894
|
+
const scope =
|
|
895
|
+
prefix !== undefined
|
|
896
|
+
? { prefix }
|
|
897
|
+
: { routes: routes.map((route) => ({ method: route.method, path: route.path })) }
|
|
898
|
+
for (const [originals, appended] of pairs) {
|
|
899
|
+
if (originals.length > 0) tagScoped(originals, appended.slice(-originals.length), scope)
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
820
903
|
/** Run the deadness check exactly once. A hook whose recorded route count equals the final route count
|
|
821
904
|
* had no route declared after it - unambiguously dead, since the scoping feature (a hook that covers
|
|
822
905
|
* only later routes) always leaves at least one such route. Zero-route apps are exempt (different
|
|
@@ -824,13 +907,20 @@ const recordScopedHook = (key: object, routesAtPush: number, kind: string): void
|
|
|
824
907
|
const sealHookAudit = (
|
|
825
908
|
key: object,
|
|
826
909
|
total: number,
|
|
910
|
+
sockets: number,
|
|
827
911
|
logger: { warn(message: string): void },
|
|
828
912
|
): void => {
|
|
829
913
|
const state = hookAudit.get(key)
|
|
830
914
|
if (state === undefined || state.checked) return
|
|
831
915
|
state.checked = true
|
|
832
|
-
if (state.policy === "off" || total === 0) return
|
|
833
|
-
const dead = state.sites.filter(
|
|
916
|
+
if (state.policy === "off" || total + sockets === 0) return
|
|
917
|
+
const dead = state.sites.filter(
|
|
918
|
+
(hook) =>
|
|
919
|
+
hook.routesAtPush ===
|
|
920
|
+
(hook.kind === "around" || hook.kind === "aroundCapability" || hook.kind === "afterHandle"
|
|
921
|
+
? total
|
|
922
|
+
: total + sockets),
|
|
923
|
+
)
|
|
834
924
|
if (dead.length === 0) return
|
|
835
925
|
const detail = dead.map((hook) => ` ${hook.kind}() at ${hook.site ?? "<unknown>"}`).join("\n")
|
|
836
926
|
const message =
|
|
@@ -865,7 +955,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
865
955
|
private readonly catalog: RouteCatalog
|
|
866
956
|
/** WebSocket routes, matched separately at upgrade time (a GET + `Upgrade: websocket`). */
|
|
867
957
|
private readonly wsRouter: Router<WsEntry>
|
|
868
|
-
|
|
958
|
+
/** Each socket path, its declared capabilities, the evidence declarations its handshake captured
|
|
959
|
+
* and the chain it runs; reflection reads it. */
|
|
960
|
+
private readonly wsRoutes: {
|
|
961
|
+
readonly path: string
|
|
962
|
+
readonly capabilities: readonly string[] | undefined
|
|
963
|
+
readonly assurance: readonly AssuranceDeclaration[]
|
|
964
|
+
readonly chain: RouteEntry
|
|
965
|
+
}[]
|
|
869
966
|
/** `wsChanges` when `wsMountsLive` was last worked out; -1 until then. */
|
|
870
967
|
private wsMountsCheckedAt = -1
|
|
871
968
|
/** Whether an app mounted here takes WebSocket upgrades, as of `wsMountsCheckedAt`. */
|
|
@@ -886,6 +983,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
886
983
|
| undefined
|
|
887
984
|
private readonly maxBodyBytes: number
|
|
888
985
|
private readonly protoPoisoning: "reject" | "strip" | "ignore"
|
|
986
|
+
private readonly formOrigins: FormOrigins | undefined
|
|
889
987
|
/** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
|
|
890
988
|
private readonly trustBodyFraming: boolean
|
|
891
989
|
private readonly wsMaxPayloadBytes: number
|
|
@@ -899,7 +997,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
899
997
|
private readonly acceptInboundDeadlines: boolean
|
|
900
998
|
private readonly maxInboundDeadlineMs: number
|
|
901
999
|
private readonly deadlineAdmissionOptions: DeadlineAdmissionOptions
|
|
902
|
-
private readonly gracefulSignals:
|
|
1000
|
+
private readonly gracefulSignals: { readonly drainMs?: number } | undefined
|
|
903
1001
|
private readonly stopHooks: StopHook[]
|
|
904
1002
|
private readonly fetchMounts: FetchMount<EnvOf<Ctx>>[]
|
|
905
1003
|
private mountOrder: number
|
|
@@ -980,9 +1078,19 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
980
1078
|
/** Body/raw response hooks need a framework-payload marker; keep that decision per app. */
|
|
981
1079
|
private responseBodyTag: object | undefined
|
|
982
1080
|
private readonly responseBodyOwners: Set<object>
|
|
983
|
-
private readonly finalizeResponse = (result: unknown, set: CtxSet): Response =>
|
|
984
|
-
toResponse(
|
|
1081
|
+
private readonly finalizeResponse = (result: unknown, set: CtxSet, ctx?: RawContext): Response =>
|
|
1082
|
+
toResponse(
|
|
1083
|
+
result as HandlerResult,
|
|
1084
|
+
set,
|
|
1085
|
+
this.responseBodyTag,
|
|
1086
|
+
this.staticResponseHeaders,
|
|
1087
|
+
// Response hooks write next, and they run for this same request object.
|
|
1088
|
+
ctx !== undefined && this.onResponseHooks.length > 0 ? ctx[CONTEXT_REQUEST] : undefined,
|
|
1089
|
+
)
|
|
985
1090
|
private readonly responseRequests: WeakMap<Request, Request>
|
|
1091
|
+
/** A request the WebSocket handshake check ran the onRequest hooks on and then passed to HTTP →
|
|
1092
|
+
* what the hooks returned, so `fetch` routes that instead of running the hooks a second time. */
|
|
1093
|
+
private wsHookedPasses: WeakMap<object, Request> | undefined
|
|
986
1094
|
/** Original source → request observed by generic request hooks, including in-place mutations. */
|
|
987
1095
|
private readonly responseSources: WeakMap<object, Request>
|
|
988
1096
|
/** Memoized NodeRequestContext per plain-`Request` source - see {@link nodeRequestContextOf}. */
|
|
@@ -1006,11 +1114,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1006
1114
|
this.routePrefix = ""
|
|
1007
1115
|
this.catalog = new RouteCatalog()
|
|
1008
1116
|
this.wsRouter = new Router<WsEntry>()
|
|
1009
|
-
this.
|
|
1117
|
+
this.wsRoutes = []
|
|
1010
1118
|
this.topics = undefined
|
|
1011
1119
|
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
1012
1120
|
assertByteLimit(maxBodyBytes, "maxBodyBytes")
|
|
1013
1121
|
this.protoPoisoning = options.protoPoisoning ?? "reject"
|
|
1122
|
+
this.formOrigins = options.formOrigins
|
|
1014
1123
|
this.trustBodyFraming = options.trustBodyFraming ?? false
|
|
1015
1124
|
const wsMaxPayloadBytes = options.wsMaxPayloadBytes ?? maxBodyBytes
|
|
1016
1125
|
assertByteLimit(wsMaxPayloadBytes, "wsMaxPayloadBytes")
|
|
@@ -1018,7 +1127,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1018
1127
|
this.wsMaxPayloadBytes = wsMaxPayloadBytes
|
|
1019
1128
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 0
|
|
1020
1129
|
this.wsUpgradeTimeoutMs = options.wsUpgradeTimeoutMs ?? DEFAULT_WS_UPGRADE_TIMEOUT_MS
|
|
1021
|
-
|
|
1130
|
+
const trust = options.clientIp
|
|
1131
|
+
if (
|
|
1132
|
+
trust !== undefined &&
|
|
1133
|
+
!("header" in trust) &&
|
|
1134
|
+
!(Number.isSafeInteger(trust.trustedHops) && trust.trustedHops >= 0)
|
|
1135
|
+
) {
|
|
1136
|
+
throw new RangeError("clientIp trustedHops must be a non-negative integer")
|
|
1137
|
+
}
|
|
1138
|
+
this.clientIpTrust = trust
|
|
1022
1139
|
this.acceptInboundDeadlines = options.acceptInboundDeadlines ?? false
|
|
1023
1140
|
this.maxInboundDeadlineMs = options.maxInboundDeadlineMs ?? 30_000
|
|
1024
1141
|
this.deadlineAdmissionOptions = Object.freeze({
|
|
@@ -1034,7 +1151,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1034
1151
|
if (!Number.isFinite(this.maxInboundDeadlineMs) || this.maxInboundDeadlineMs <= 0) {
|
|
1035
1152
|
throw new RangeError("maxInboundDeadlineMs must be a finite positive number")
|
|
1036
1153
|
}
|
|
1037
|
-
this.gracefulSignals =
|
|
1154
|
+
this.gracefulSignals =
|
|
1155
|
+
options.gracefulSignals === true ? {} : options.gracefulSignals || undefined
|
|
1038
1156
|
this.stopHooks = []
|
|
1039
1157
|
this.fetchMounts = []
|
|
1040
1158
|
this.mountOrder = 0
|
|
@@ -1083,6 +1201,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1083
1201
|
this.responseBodyTag = undefined
|
|
1084
1202
|
this.responseBodyOwners = new Set()
|
|
1085
1203
|
this.responseRequests = new WeakMap()
|
|
1204
|
+
this.wsHookedPasses = undefined
|
|
1086
1205
|
this.responseSources = new WeakMap()
|
|
1087
1206
|
this.nodeContexts = new WeakMap()
|
|
1088
1207
|
this.appliedPlugins = new Map()
|
|
@@ -1187,6 +1306,20 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1187
1306
|
opaque: options.opaque,
|
|
1188
1307
|
app,
|
|
1189
1308
|
}
|
|
1309
|
+
// Mounts run before routes: a request under this prefix that no mount on the way strips comes back
|
|
1310
|
+
// here unchanged until the stack overflows, which takes the process down. Dev-time only, like the
|
|
1311
|
+
// hook audit: a production define folds this away, and the first dev request would hit it anyway.
|
|
1312
|
+
if (
|
|
1313
|
+
hookAuditRuntime &&
|
|
1314
|
+
process.env.NODE_ENV !== "production" &&
|
|
1315
|
+
!mount.stripPrefix &&
|
|
1316
|
+
mountLeadsBack(app, this, mount.path, (server) => server.fetchMounts, new Set())
|
|
1317
|
+
) {
|
|
1318
|
+
throw new RouteConfigError(
|
|
1319
|
+
"MOUNT_CYCLE",
|
|
1320
|
+
`mount ${mount.path}: the app leads back to this one without stripPrefix`,
|
|
1321
|
+
)
|
|
1322
|
+
}
|
|
1190
1323
|
this.fetchMounts.push(mount)
|
|
1191
1324
|
this.fetchMounts.sort(compareMounts)
|
|
1192
1325
|
this.preRouteMountCount += 1
|
|
@@ -1214,8 +1347,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1214
1347
|
> {
|
|
1215
1348
|
this.assertConfigurable("derive()")
|
|
1216
1349
|
this.derives.push(fn as unknown as RawDerive)
|
|
1217
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1218
|
-
recordScopedHook(this, this.catalog.size, "derive")
|
|
1350
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1351
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "derive")
|
|
1352
|
+
tagHook(fn, "derive", this)
|
|
1353
|
+
}
|
|
1219
1354
|
return this as unknown as Server<
|
|
1220
1355
|
R,
|
|
1221
1356
|
Ctx & ContextExtensionOf<ReturnType<H>>,
|
|
@@ -1252,6 +1387,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1252
1387
|
throw new TypeError('authenticate() stage mode must be "sync" or "async"')
|
|
1253
1388
|
}
|
|
1254
1389
|
this.authStages.push(stage as unknown as RawAuthStage)
|
|
1390
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1391
|
+
tagHook(stage, "authenticate", this)
|
|
1255
1392
|
return this as unknown as Server<R, Ctx & { readonly principal: Principal }, HookOutput>
|
|
1256
1393
|
}
|
|
1257
1394
|
|
|
@@ -1259,19 +1396,21 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1259
1396
|
this.assertConfigurable("decorate()")
|
|
1260
1397
|
this.decorations[key] = value
|
|
1261
1398
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1262
|
-
recordScopedHook(this, this.catalog.size, "decorate")
|
|
1399
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "decorate")
|
|
1263
1400
|
return this as unknown as Server<R, Ctx & Record<K, V>, HookOutput>
|
|
1264
1401
|
}
|
|
1265
1402
|
|
|
1266
1403
|
/**
|
|
1267
|
-
* Run before routing on the raw request. Return a `Response`
|
|
1268
|
-
* `Request` to continue routing with a rewritten method/URL/headers.
|
|
1404
|
+
* Run before routing on the raw request. Return a `Response` or a `status()` result to
|
|
1405
|
+
* short-circuit, or a replacement `Request` to continue routing with a rewritten method/URL/headers.
|
|
1406
|
+
* Global.
|
|
1269
1407
|
*/
|
|
1270
1408
|
onRequest(
|
|
1271
1409
|
fn: (req: Request, platform?: Platform<EnvOf<Ctx>>) => MaybePromise<OnRequestResult>,
|
|
1272
1410
|
): this {
|
|
1273
1411
|
this.assertConfigurable("onRequest()")
|
|
1274
1412
|
this.onRequestHooks.push(fn as RawOnRequest)
|
|
1413
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") tagHook(fn, "onRequest", this)
|
|
1275
1414
|
this.onNodeRequestHooks.push(undefined)
|
|
1276
1415
|
this.nodeRequestHooksComplete = false
|
|
1277
1416
|
this.bunNativeRequestHooksSafe = false
|
|
@@ -1288,8 +1427,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1288
1427
|
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1289
1428
|
this.assertConfigurable("beforeHandle()")
|
|
1290
1429
|
this.beforeHandleHooks.push(fn as unknown as RawBeforeHandle)
|
|
1291
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1292
|
-
recordScopedHook(this, this.catalog.size, "beforeHandle")
|
|
1430
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1431
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "beforeHandle")
|
|
1432
|
+
tagHook(fn, "beforeHandle", this)
|
|
1433
|
+
}
|
|
1293
1434
|
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1294
1435
|
}
|
|
1295
1436
|
|
|
@@ -1306,8 +1447,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1306
1447
|
>(fn: H): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1307
1448
|
this.assertConfigurable("around()")
|
|
1308
1449
|
this.aroundHooks.push(fn as unknown as RawAround)
|
|
1309
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1450
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1310
1451
|
recordScopedHook(this, this.catalog.size, "around")
|
|
1452
|
+
tagHook(fn, "around", this)
|
|
1453
|
+
}
|
|
1311
1454
|
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1312
1455
|
}
|
|
1313
1456
|
|
|
@@ -1355,8 +1498,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1355
1498
|
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1356
1499
|
this.assertConfigurable("afterHandle()")
|
|
1357
1500
|
this.afterHandleHooks.push(fn as unknown as RawAfterHandle)
|
|
1358
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1501
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1359
1502
|
recordScopedHook(this, this.catalog.size, "afterHandle")
|
|
1503
|
+
tagHook(fn, "afterHandle", this)
|
|
1504
|
+
}
|
|
1360
1505
|
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1361
1506
|
}
|
|
1362
1507
|
|
|
@@ -1368,8 +1513,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1368
1513
|
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1369
1514
|
this.assertConfigurable("onError()")
|
|
1370
1515
|
this.onErrorHooks.push(fn as unknown as RawErrorHandler)
|
|
1371
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1372
|
-
recordScopedHook(this, this.catalog.size, "onError")
|
|
1516
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1517
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "onError")
|
|
1518
|
+
tagHook(fn, "onError", this)
|
|
1519
|
+
}
|
|
1373
1520
|
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1374
1521
|
}
|
|
1375
1522
|
|
|
@@ -1377,6 +1524,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1377
1524
|
onResponse(fn: (response: Response, req: Request) => MaybePromise<Response>): this {
|
|
1378
1525
|
this.assertConfigurable("onResponse()")
|
|
1379
1526
|
this.onResponseHooks.push(fn)
|
|
1527
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") tagHook(fn, "onResponse", this)
|
|
1380
1528
|
this.onNodeResponseHooks.push(undefined)
|
|
1381
1529
|
this.nodeResponseHooksComplete = false
|
|
1382
1530
|
this.bunNativeResponseHeadersOnly = false
|
|
@@ -1409,6 +1557,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1409
1557
|
*/
|
|
1410
1558
|
responseHeaders(record: Readonly<Record<string, string>>): this {
|
|
1411
1559
|
this.assertConfigurable("responseHeaders()")
|
|
1560
|
+
if (
|
|
1561
|
+
hookAuditRuntime &&
|
|
1562
|
+
process.env.NODE_ENV !== "production" &&
|
|
1563
|
+
this.onResponseHooks.length === 0
|
|
1564
|
+
)
|
|
1565
|
+
tagStatic(this, Object.keys(record), record)
|
|
1412
1566
|
return this.addStaticResponseHeaders(normalizeStaticResponseHeaders(record))
|
|
1413
1567
|
}
|
|
1414
1568
|
|
|
@@ -1427,13 +1581,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1427
1581
|
})
|
|
1428
1582
|
return this
|
|
1429
1583
|
}
|
|
1430
|
-
// Route the hook through the same seam the static tier uses. On a framework response
|
|
1431
|
-
//
|
|
1432
|
-
//
|
|
1433
|
-
//
|
|
1434
|
-
//
|
|
1584
|
+
// Route the hook through the same seam the static tier uses. On a framework response with a
|
|
1585
|
+
// body it applies the defaults in place and returns the SAME object, so the serialized-body
|
|
1586
|
+
// marker survives for later `onResponseBody`/`onResponseRaw` observers and the Node writer;
|
|
1587
|
+
// reconstructing a `Response` here would strip that marker and reclassify the Node outcome from
|
|
1588
|
+
// `json` to a generic `response`. A body-less or guarded response is copied, marker included.
|
|
1435
1589
|
const statics = buildStaticResponseHeaders(record, this.inheritedStatics)
|
|
1436
|
-
this.onResponseHooks.push((response) =>
|
|
1590
|
+
this.onResponseHooks.push((response, req) =>
|
|
1591
|
+
applyStaticResponseHeaders(response, statics, req),
|
|
1592
|
+
)
|
|
1593
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1594
|
+
tagHook(this.onResponseHooks.at(-1), "responseHeaders", this)
|
|
1437
1595
|
this.onNodeResponseHooks.push(undefined)
|
|
1438
1596
|
this.nodeResponseHooksComplete = false
|
|
1439
1597
|
return this
|
|
@@ -1478,6 +1636,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1478
1636
|
): this {
|
|
1479
1637
|
this.assertConfigurable("onResponseFinalized()")
|
|
1480
1638
|
this.onResponseFinalizedHooks.push(fn)
|
|
1639
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1640
|
+
tagHook(fn, "onResponseFinalized", this)
|
|
1481
1641
|
return this
|
|
1482
1642
|
}
|
|
1483
1643
|
|
|
@@ -1570,9 +1730,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1570
1730
|
this.activeAssurance.push(...pluginOnly)
|
|
1571
1731
|
const inherited = this.inheritedPlugins
|
|
1572
1732
|
this.inheritedPlugins = undefined
|
|
1733
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") enterPlugin(name)
|
|
1573
1734
|
try {
|
|
1574
1735
|
return arg(this)
|
|
1575
1736
|
} finally {
|
|
1737
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") leavePlugin()
|
|
1576
1738
|
this.inheritedPlugins = inherited
|
|
1577
1739
|
// Remove only this plugin's temporary evidence. Nested assured plugins may deliberately leave
|
|
1578
1740
|
// subsequent evidence active, so truncating the whole array would lose real ordering semantics.
|
|
@@ -1602,6 +1764,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1602
1764
|
if (arg.onRequest !== undefined) {
|
|
1603
1765
|
this.assertConfigurable("onRequest()")
|
|
1604
1766
|
this.onRequestHooks.push(arg.onRequest as RawOnRequest)
|
|
1767
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1768
|
+
tagHook(arg.onRequest, "onRequest", this)
|
|
1605
1769
|
this.onNodeRequestHooks.push(arg.onNodeRequest)
|
|
1606
1770
|
if (arg.onNodeRequest === undefined) this.nodeRequestHooksComplete = false
|
|
1607
1771
|
if (!(arg as unknown as Record<symbol, unknown>)[BUN_NATIVE_REQUEST_SAFE]) {
|
|
@@ -1616,6 +1780,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1616
1780
|
if (arg.onResponse !== undefined) {
|
|
1617
1781
|
this.assertConfigurable("onResponse()")
|
|
1618
1782
|
this.onResponseHooks.push(arg.onResponse)
|
|
1783
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1784
|
+
tagHook(arg.onResponse, "onResponse", this)
|
|
1619
1785
|
this.onNodeResponseHooks.push(arg.onNodeResponse)
|
|
1620
1786
|
if (arg.onNodeResponse === undefined) this.nodeResponseHooksComplete = false
|
|
1621
1787
|
else this.hasRawNodeResponseHook = true
|
|
@@ -1672,8 +1838,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1672
1838
|
}
|
|
1673
1839
|
|
|
1674
1840
|
/**
|
|
1675
|
-
* Register a **typed SSE route** -
|
|
1676
|
-
* payloads are contracted by `schema.sse`.
|
|
1841
|
+
* Register a **typed SSE route** - an endpoint streaming `text/event-stream` whose event
|
|
1842
|
+
* payloads are contracted by `schema.sse`. It is a GET route, or a POST route when the schema
|
|
1843
|
+
* declares a `body` (a GET carries none): the body validates as on any POST, and the client sends
|
|
1844
|
+
* it with `.subscribe(onEvent, { body })` - the shape of a chat stream that posts its prompt. The handler receives the validated context plus a
|
|
1677
1845
|
* {@link TypedSSEStream}: `stream.send(event)` is compile-time-checked against the schema and
|
|
1678
1846
|
* JSON-serialized into the SSE `data:` field. The typed client sees the marker and grows a
|
|
1679
1847
|
* `.subscribe(onEvent)` for the route with the same payload type - end-to-end typed streaming.
|
|
@@ -1688,9 +1856,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1688
1856
|
*
|
|
1689
1857
|
* // client: const off = api.feed.subscribe((post) => console.log(post.title))
|
|
1690
1858
|
*
|
|
1691
|
-
* `init` passes through to the underlying {@link sse} helper (`keepAlive`,
|
|
1692
|
-
* connection closes when the handler resolves, `stream.close()` runs, or the
|
|
1693
|
-
* (`stream.signal`). Query/body schemas validate exactly as on any other route.
|
|
1859
|
+
* `init` passes through to the underlying {@link sse} helper (`keepAlive`, `maxBufferedBytes`,
|
|
1860
|
+
* extra headers). The connection closes when the handler resolves, `stream.close()` runs, or the
|
|
1861
|
+
* client disconnects (`stream.signal`). Query/body schemas validate exactly as on any other route.
|
|
1694
1862
|
*/
|
|
1695
1863
|
sse<Path extends string, S extends RouteSchema & { sse: StandardSchemaV1 }>(
|
|
1696
1864
|
path: Path,
|
|
@@ -1701,14 +1869,19 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1701
1869
|
) => void | Promise<void>,
|
|
1702
1870
|
init?: SSEInit,
|
|
1703
1871
|
): Server<
|
|
1704
|
-
AddRoute<R,
|
|
1872
|
+
AddRoute<R, SseMethodOf<S>, Path, RouteInfoFor<Path, S, Response, HookOutput>>,
|
|
1705
1873
|
Ctx,
|
|
1706
1874
|
HookOutput
|
|
1707
1875
|
> {
|
|
1708
1876
|
const handler = (context: Context<Path, S> & Ctx): Response =>
|
|
1709
1877
|
requireSseRuntime(this.sseRuntime).response(context, (stream) => run(context, stream), init)
|
|
1710
|
-
return this.route(
|
|
1711
|
-
|
|
1878
|
+
return this.route(
|
|
1879
|
+
schema.body ? "POST" : "GET",
|
|
1880
|
+
path,
|
|
1881
|
+
schema,
|
|
1882
|
+
handler as unknown as ErasedHandler,
|
|
1883
|
+
) as Server<
|
|
1884
|
+
AddRoute<R, SseMethodOf<S>, Path, RouteInfoFor<Path, S, Response, HookOutput>>,
|
|
1712
1885
|
Ctx,
|
|
1713
1886
|
HookOutput
|
|
1714
1887
|
>
|
|
@@ -1927,8 +2100,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1927
2100
|
|
|
1928
2101
|
/**
|
|
1929
2102
|
* Register a **WebSocket** route. The connection upgrades on a `GET` to `path` carrying
|
|
1930
|
-
* `Upgrade: websocket
|
|
1931
|
-
*
|
|
2103
|
+
* `Upgrade: websocket`. The handshake runs the chain an HTTP `GET` registered here would: the
|
|
2104
|
+
* `authenticate()` stages, `derive`s and `beforeHandle`s added before this call, then the optional
|
|
2105
|
+
* `handler.upgrade(c)`. Any of them may reject (a `Response`, a `status(...)`, a failed auth stage);
|
|
2106
|
+
* `upgrade()` may seed per-connection `ws.data`. To keep a socket public, register it before the
|
|
2107
|
+
* guard. WebSockets are served by the
|
|
1932
2108
|
* adapter (`listen()`, `@nifrajs/node`, `@nifrajs/deno`, `toFetchHandler`) - not by bare `app.fetch`, which
|
|
1933
2109
|
* has no socket (a WS path through `app.fetch` is a normal HTTP response).
|
|
1934
2110
|
*
|
|
@@ -1947,7 +2123,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1947
2123
|
Path extends string = string,
|
|
1948
2124
|
>(
|
|
1949
2125
|
path: Path,
|
|
1950
|
-
handler: WebSocketHandler<Data, EnvOf<Ctx>, Schema, Send>,
|
|
2126
|
+
handler: WebSocketHandler<Data, EnvOf<Ctx>, Schema, Send, Ctx>,
|
|
1951
2127
|
): string extends Path
|
|
1952
2128
|
? Server<R, Ctx, HookOutput>
|
|
1953
2129
|
: Server<AddRoute<R, "WS", Path, WsRouteInfoFor<Path, Schema, Send>>, Ctx, HookOutput> {
|
|
@@ -1967,9 +2143,25 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1967
2143
|
this.topics ??= runtime.createTopics()
|
|
1968
2144
|
// A `messageSchema` wraps `message` with validation once, here - every adapter then dispatches
|
|
1969
2145
|
// already-validated, typed messages (Bun/Deno/Node/Workers) with no per-adapter code.
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
2146
|
+
const wrapped = runtime.wrapHandler(handler as WebSocketHandler, this.protoPoisoning)
|
|
2147
|
+
const upgrade = runtime.upgradeStep(wrapped)
|
|
2148
|
+
for (const form of expandOptionalParams(path)) {
|
|
2149
|
+
// The chain an HTTP `GET` to `form` would capture now, ending in `upgrade()` instead of a
|
|
2150
|
+
// handler. `afterHandle` and `around` are left out: they wrap a response, and an accepted
|
|
2151
|
+
// handshake has none.
|
|
2152
|
+
const { entry, assurance, descriptor } = this.prepareRoute(
|
|
2153
|
+
"WS",
|
|
2154
|
+
form,
|
|
2155
|
+
wrapped.capabilities === undefined ? undefined : { capabilities: wrapped.capabilities },
|
|
2156
|
+
upgrade,
|
|
2157
|
+
)
|
|
2158
|
+
const chain = { ...entry, afterHandle: [] }
|
|
2159
|
+
this.wsRouter.add("GET", form, {
|
|
2160
|
+
handler: wrapped,
|
|
2161
|
+
chain: { ...chain, program: compileRouteProgram({ ...chain, bodySchema: undefined }) },
|
|
2162
|
+
})
|
|
2163
|
+
this.wsRoutes.push({ path: form, capabilities: descriptor.capabilities, assurance, chain })
|
|
2164
|
+
}
|
|
1973
2165
|
wsChanges += 1
|
|
1974
2166
|
return this as never
|
|
1975
2167
|
}
|
|
@@ -2265,30 +2457,6 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2265
2457
|
}
|
|
2266
2458
|
}
|
|
2267
2459
|
|
|
2268
|
-
/** The idempotency lane's bridge back into the normal matched lanes, resolved to a concrete Response
|
|
2269
|
-
* (the lane buffers the body, then replays it through the route's real validation + handler). */
|
|
2270
|
-
private idempotencyRunLanes(
|
|
2271
|
-
buffered: RequestSource,
|
|
2272
|
-
platform: Platform | undefined,
|
|
2273
|
-
entry: RouteEntry,
|
|
2274
|
-
params: Record<string, string>,
|
|
2275
|
-
search: string | undefined,
|
|
2276
|
-
): Promise<Response> {
|
|
2277
|
-
return Promise.resolve(
|
|
2278
|
-
this.runMatchedLanes(
|
|
2279
|
-
buffered,
|
|
2280
|
-
platform,
|
|
2281
|
-
entry,
|
|
2282
|
-
params,
|
|
2283
|
-
search,
|
|
2284
|
-
this.finalizeResponse,
|
|
2285
|
-
this.wrapWebResponse,
|
|
2286
|
-
this.webResponseTimeout,
|
|
2287
|
-
false,
|
|
2288
|
-
),
|
|
2289
|
-
)
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
2460
|
/** @internal Symbol-keyed install seam for the `responseContract()` plugin. Off the public typed surface. */
|
|
2293
2461
|
[INSTALL_RESPONSE_CONTRACT](runtime: ResponseContractRuntime): void {
|
|
2294
2462
|
this.assertConfigurable("responseContract()")
|
|
@@ -2302,6 +2470,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2302
2470
|
assertConfigurable: (operation) => this.assertConfigurable(operation),
|
|
2303
2471
|
addResponseHook: (web, node, headerOnly = false) => {
|
|
2304
2472
|
this.onResponseHooks.push(web)
|
|
2473
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2474
|
+
tagHook(web, headerOnly ? "responseHeaders" : "onResponse", this)
|
|
2305
2475
|
this.onNodeResponseHooks.push(node)
|
|
2306
2476
|
if (node === undefined) this.nodeResponseHooksComplete = false
|
|
2307
2477
|
if (!headerOnly) this.bunNativeResponseHeadersOnly = false
|
|
@@ -2343,6 +2513,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2343
2513
|
this.nodeOutcomeRuntime = runtime
|
|
2344
2514
|
}
|
|
2345
2515
|
|
|
2516
|
+
/** @internal An adapter reports a failure no response hook saw (a hook that threw past `fetch`, a
|
|
2517
|
+
* response the runtime refused to write), logged as Bun's `listen()` logs one. */
|
|
2518
|
+
[REPORT_ERROR](error: unknown): void {
|
|
2519
|
+
emitRequestErrorLog(this.logger, this.errorLogDetail, error, undefined)
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2346
2522
|
/** @internal Symbol-keyed install seam for the `streaming()` plugin. Off the public typed surface. */
|
|
2347
2523
|
[INSTALL_SSE](runtime: SseRuntime): void {
|
|
2348
2524
|
this.assertConfigurable("streaming()")
|
|
@@ -2363,7 +2539,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2363
2539
|
* counts. `seen` ends a mount cycle (an app mounted under itself to alias a prefix).
|
|
2364
2540
|
*/
|
|
2365
2541
|
[GET_WS_RUNTIME](seen?: Set<unknown>): WsRuntime | null | undefined {
|
|
2366
|
-
return this.
|
|
2542
|
+
return this.wsRoutes.length > 0 ? this.wsRuntime : this.mountedWsRuntime(seen)
|
|
2367
2543
|
}
|
|
2368
2544
|
|
|
2369
2545
|
/** The `[GET_WS_RUNTIME]` answer for the apps mounted here alone, whatever this app's own routes. */
|
|
@@ -2404,15 +2580,36 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2404
2580
|
* `derive`/`decorate`/`beforeHandle`/`afterHandle`/`onError`/`around` apply to its routes
|
|
2405
2581
|
* exactly as they did standalone, so a group wires its own plugins. The same locality rule
|
|
2406
2582
|
* covers the group's `onRequest` hooks: they run only for requests that route to the GROUP's
|
|
2407
|
-
* routes (a `bodyLimit()` mounted on an uploads group must not start gating the whole app
|
|
2583
|
+
* routes, and for an `OPTIONS` preflight to a path they serve (a `bodyLimit()` mounted on an uploads group must not start gating the whole app
|
|
2408
2584
|
* because the app composed it in), and the group's global assurance follows its hooks onto
|
|
2409
2585
|
* exactly those routes. A group with hooks but NO routes is a middleware bundle - its hooks
|
|
2410
2586
|
* can only mean app-wide intent, so they are appended globally, unchanged. Response-side hooks
|
|
2411
2587
|
* (`onResponse`/`onResponseFinalized`) are appended to this server's. This server's
|
|
2412
2588
|
* route-scoped chains do NOT retroactively wrap merged routes (order-scoped, like routes
|
|
2413
2589
|
* registered before a `derive`). Fail closed: a path+method collision throws
|
|
2414
|
-
* `RouteConfigError` at merge time, and a group with WebSocket routes is refused
|
|
2415
|
-
* those on the parent).
|
|
2590
|
+
* `RouteConfigError` at merge time, and a group with WebSocket routes or mounts is refused
|
|
2591
|
+
* (register those on the parent).
|
|
2592
|
+
*
|
|
2593
|
+
* What this server's `.use()`, made BEFORE the merge, covers on a merged route versus a
|
|
2594
|
+
* `group()` route:
|
|
2595
|
+
*
|
|
2596
|
+
* | installed on this server | group() route | merge()d route |
|
|
2597
|
+
* | ------------------------------------------ | ------------- | ------------------------------ |
|
|
2598
|
+
* | derive/decorate/beforeHandle/afterHandle, | yes | no - wire it on the merged |
|
|
2599
|
+
* | onError/around, authenticate, route-scoped | | server |
|
|
2600
|
+
* | assurance | | |
|
|
2601
|
+
* | onRequest/onResponse/responseHeaders hooks | yes | yes (app-wide) |
|
|
2602
|
+
* | responseContract() | yes | yes, when the route declares a |
|
|
2603
|
+
* | | | response and its own server |
|
|
2604
|
+
* | | | installed no contract |
|
|
2605
|
+
* | effectLedger() | yes | yes, when the route declares |
|
|
2606
|
+
* | | | capabilities and has no ledger |
|
|
2607
|
+
* | idempotency() | yes | the merged server's own; a |
|
|
2608
|
+
* | | | route declaring it without one |
|
|
2609
|
+
* | | | is refused at registration |
|
|
2610
|
+
*
|
|
2611
|
+
* The response contract and the ledger can only add a check or a record, so a merged route is
|
|
2612
|
+
* recompiled under them. A runtime installed AFTER the merge covers neither kind of route.
|
|
2416
2613
|
*/
|
|
2417
2614
|
merge<R2 extends Registry, Ctx2, HookOutput2>(
|
|
2418
2615
|
other: Server<R2, Ctx2, HookOutput2>,
|
|
@@ -2432,10 +2629,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2432
2629
|
)
|
|
2433
2630
|
}
|
|
2434
2631
|
const source = other as unknown as Server<Registry, EmptyContext>
|
|
2435
|
-
if (source.
|
|
2632
|
+
if (source.wsRoutes.length > 0 || source.fetchMounts.length > 0) {
|
|
2436
2633
|
throw new RouteConfigError(
|
|
2437
2634
|
"INVALID_PATH",
|
|
2438
|
-
"merge() does not carry WebSocket routes - register
|
|
2635
|
+
"merge() does not carry WebSocket routes or mounts - register them on the parent server",
|
|
2439
2636
|
)
|
|
2440
2637
|
}
|
|
2441
2638
|
this.adopt(source, undefined, 0)
|
|
@@ -2457,7 +2654,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2457
2654
|
* The builder receives a scope that INHERITS this server's route chain as it stands at the call
|
|
2458
2655
|
* (`derive`/`decorate`/`authenticate`/`beforeHandle`/`afterHandle`/`onError`/`around`, assurance,
|
|
2459
2656
|
* capability interceptors, installed runtimes, applied plugins), exactly as a route declared here
|
|
2460
|
-
* would. A plugin this server already applied is in effect for the group: the group's `use()` of
|
|
2657
|
+
* would. {@link merge} documents how that differs for a merged server. A plugin this server already applied is in effect for the group: the group's `use()` of
|
|
2461
2658
|
* that same instance does nothing, and its `use()` of another instance under the same name throws a
|
|
2462
2659
|
* `RouteConfigError` (`PLUGIN_RECONFIGURED`), because that instance could never replace the parent's.
|
|
2463
2660
|
* A plugin the group applies still shares the parent's copy of a dependency it `use()`s itself.
|
|
@@ -2521,7 +2718,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2521
2718
|
throw new TypeError(`${label} builder must return its group synchronously`)
|
|
2522
2719
|
}
|
|
2523
2720
|
if (
|
|
2524
|
-
scope.
|
|
2721
|
+
scope.wsRoutes.length > 0 ||
|
|
2525
2722
|
scope.fetchMounts.length > 0 ||
|
|
2526
2723
|
scope.catalog.entries().some((route) => route.descriptor.tool !== undefined)
|
|
2527
2724
|
) {
|
|
@@ -2531,7 +2728,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2531
2728
|
)
|
|
2532
2729
|
}
|
|
2533
2730
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2534
|
-
sealHookAudit(scope, scope.catalog.size, this.logger)
|
|
2731
|
+
sealHookAudit(scope, scope.catalog.size, scope.wsRoutes.length, this.logger)
|
|
2535
2732
|
this.adopt(scope, fullPrefix, this.globalAssurance.length)
|
|
2536
2733
|
return this as unknown as Server<R & PrefixRegistry<P, R2>, Ctx, HookOutput>
|
|
2537
2734
|
}
|
|
@@ -2565,7 +2762,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2565
2762
|
})
|
|
2566
2763
|
: (route: CatalogRoute): CatalogRoute => route
|
|
2567
2764
|
this.catalog.addBatch(
|
|
2568
|
-
sourceRoutes.map((route) =>
|
|
2765
|
+
sourceRoutes.map((route) =>
|
|
2766
|
+
this.bindFusedRuntime(
|
|
2767
|
+
foldedAssurance(
|
|
2768
|
+
adoptRouteRuntimes(route, this.responseContractRuntime, this.effectLedgerRuntime),
|
|
2769
|
+
),
|
|
2770
|
+
),
|
|
2771
|
+
),
|
|
2569
2772
|
)
|
|
2570
2773
|
// Resolved idempotency/ledger route entries carry their own store/sink configuration, while the
|
|
2571
2774
|
// runtime object supplies the generic execution machinery. If the parent already has a runtime,
|
|
@@ -2593,12 +2796,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2593
2796
|
// The group's static declarations came before its own response hooks, so they are folded in
|
|
2594
2797
|
// first - and fold themselves into a hook here if this server already has one (same ordering
|
|
2595
2798
|
// rule as a direct `responseHeaders()` call).
|
|
2799
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2800
|
+
adoptStaticTags(this, source, this.onResponseHooks.length > 0)
|
|
2596
2801
|
this.addStaticResponseHeaders({ ...statics.record })
|
|
2597
2802
|
} else {
|
|
2598
2803
|
// The static tier is folded into EVERY response this server builds, so a group's static
|
|
2599
2804
|
// headers cannot join it. A hook ahead of the group's own (the sealed scope is discarded
|
|
2600
2805
|
// after adoption) keeps their meaning - defaults a value already on the response wins over.
|
|
2601
|
-
source.onResponseHooks.unshift((response) =>
|
|
2806
|
+
source.onResponseHooks.unshift((response, req) =>
|
|
2807
|
+
applyStaticResponseHeaders(response, statics, req),
|
|
2808
|
+
)
|
|
2809
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2810
|
+
tagHook(source.onResponseHooks[0], "responseHeaders", source, true)
|
|
2602
2811
|
source.onNodeResponseHooks.unshift(undefined)
|
|
2603
2812
|
source.nodeResponseHooksComplete = false
|
|
2604
2813
|
}
|
|
@@ -2608,6 +2817,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2608
2817
|
this.onResponseFinalizedHooks.push(
|
|
2609
2818
|
...scopeHooks(source.onResponseFinalizedHooks, responseScope, 1),
|
|
2610
2819
|
)
|
|
2820
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
2821
|
+
// Read before anything else is appended: the wrappers are still each list's tail.
|
|
2822
|
+
const pairs: (readonly [readonly unknown[], readonly unknown[]])[] = []
|
|
2823
|
+
if (inScope !== undefined) pairs.push([source.onRequestHooks, this.onRequestHooks])
|
|
2824
|
+
if (responseScope !== undefined) {
|
|
2825
|
+
pairs.push([source.onResponseHooks, this.onResponseHooks])
|
|
2826
|
+
pairs.push([source.onResponseFinalizedHooks, this.onResponseFinalizedHooks])
|
|
2827
|
+
}
|
|
2828
|
+
tagAdopted(this, source, prefix, sourceRoutes, pairs)
|
|
2829
|
+
}
|
|
2611
2830
|
this.nodeResponseHooksComplete &&= source.nodeResponseHooksComplete
|
|
2612
2831
|
this.bunNativeResponseHeadersOnly &&= source.bunNativeResponseHeadersOnly
|
|
2613
2832
|
this.hasRawNodeResponseHook ||= source.hasRawNodeResponseHook
|
|
@@ -2799,7 +3018,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2799
3018
|
// Seal once on the first request so test servers (no `listen()`) still get the dead-hook check.
|
|
2800
3019
|
// After that it is a single boolean read - the per-request cost the option promises to keep at zero.
|
|
2801
3020
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2802
|
-
sealHookAudit(this, this.catalog.size, this.logger)
|
|
3021
|
+
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger)
|
|
2803
3022
|
// Off path (default): straight through - one property check, no closure, no promise.
|
|
2804
3023
|
if (this.capacityGate === undefined) return this.fetchSourceInner(source, platform)
|
|
2805
3024
|
return this.admitGated(requestOf(source), () => this.fetchSourceInner(source, platform))
|
|
@@ -2983,7 +3202,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2983
3202
|
): MaybePromise<NodeServeOutcome> {
|
|
2984
3203
|
// Seal once on the Node-direct lane too - it can bypass `fetchSource` entirely (hookless outcome).
|
|
2985
3204
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2986
|
-
sealHookAudit(this, this.catalog.size, this.logger)
|
|
3205
|
+
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger)
|
|
2987
3206
|
// A paired header-only native hook can preserve the Node-direct JSON/body outcome for successful
|
|
2988
3207
|
// responses. Arbitrary onResponse transforms need a real Web Response, but a buffered outcome can
|
|
2989
3208
|
// be materialized with a direct-write marker and return to the socket path when the hook mutates
|
|
@@ -3242,7 +3461,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3242
3461
|
current = outcome
|
|
3243
3462
|
continue
|
|
3244
3463
|
}
|
|
3245
|
-
if (outcome !== undefined) return outcome
|
|
3464
|
+
if (outcome !== undefined) return webResponseOf(outcome)
|
|
3246
3465
|
}
|
|
3247
3466
|
return current
|
|
3248
3467
|
}
|
|
@@ -3257,7 +3476,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3257
3476
|
let outcome = first
|
|
3258
3477
|
for (let i = start; ; i++) {
|
|
3259
3478
|
if (outcome instanceof Request) next = outcome
|
|
3260
|
-
else if (outcome !== undefined) return outcome
|
|
3479
|
+
else if (outcome !== undefined) return webResponseOf(outcome)
|
|
3261
3480
|
if (i >= this.onRequestHooks.length) return next
|
|
3262
3481
|
const hook = this.onRequestHooks[i]
|
|
3263
3482
|
if (hook === undefined) return next
|
|
@@ -3271,11 +3490,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3271
3490
|
): MaybePromise<WebSocketUpgradeOutcome> {
|
|
3272
3491
|
// Before any header is read: Deno only materializes a request's headers when asked, and that
|
|
3273
3492
|
// costs a plain request measurably.
|
|
3274
|
-
if (this.
|
|
3493
|
+
if (this.wsRoutes.length === 0 && !this.mountsTakeUpgrades()) return WS_PASS
|
|
3275
3494
|
if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") return WS_PASS
|
|
3276
3495
|
// A WebSocket handshake is a GET (RFC 6455 section 4.1). Any other method with an Upgrade header
|
|
3277
3496
|
// is an ordinary request: its HTTP route answers it, and normal routing refuses an unknown token.
|
|
3278
3497
|
if (req.method !== "GET") return WS_PASS
|
|
3498
|
+
// The adapter answers a pass by handing this same object to `fetch`.
|
|
3499
|
+
const received = req
|
|
3279
3500
|
// The handshake routes the path `fetch` would: dot segments resolved (see `fetch`).
|
|
3280
3501
|
if (rawRequestTargets && hasDotSegment(req.url)) req = withResolvedTarget(req)
|
|
3281
3502
|
// The handshake's caller is the one `fetch` derives: hooks and `upgrade()` see the trusted IP.
|
|
@@ -3290,11 +3511,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3290
3511
|
const controller = timeoutMs > 0 ? new AbortController() : undefined
|
|
3291
3512
|
const signal = controller?.signal ?? getNeverAbortSignal()
|
|
3292
3513
|
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : undefined
|
|
3293
|
-
|
|
3294
|
-
const
|
|
3514
|
+
// A refusal carries the static response headers an HTTP answer would; a guard's already does.
|
|
3515
|
+
const refuse = (response: Response): WebSocketUpgradeOutcome => ({
|
|
3295
3516
|
kind: "reject",
|
|
3296
|
-
response:
|
|
3517
|
+
response: this.wrapWebResponse(response),
|
|
3297
3518
|
})
|
|
3519
|
+
const onTimeout = (): WebSocketUpgradeOutcome => refuse(jsonError(503, "request_timeout"))
|
|
3298
3520
|
const bounded = <T>(work: Promise<T>, onDone: () => T): Promise<T> =>
|
|
3299
3521
|
controller === undefined
|
|
3300
3522
|
? work
|
|
@@ -3307,11 +3529,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3307
3529
|
// Inspect only captured values for escapes. Scanning the full pathname repeated work the router
|
|
3308
3530
|
// already did and made every plain dynamic route pay for unrelated static path bytes.
|
|
3309
3531
|
const params = match.params === EMPTY_PARAMS ? match.params : decodeRouteParams(match.params)
|
|
3310
|
-
if (params === null) return
|
|
3311
|
-
const handler = match.payload
|
|
3312
|
-
// Non-null:
|
|
3532
|
+
if (params === null) return refuse(jsonError(400, "malformed_path"))
|
|
3533
|
+
const { handler, chain } = match.payload
|
|
3534
|
+
// Non-null: a socket route ⇒ ws() ran ⇒ `.use(websocket())` installed the runtime + registry.
|
|
3313
3535
|
const pubsub = this.topics as TopicRegistry
|
|
3314
|
-
const
|
|
3536
|
+
const runtime = this.wsRuntime as WsRuntime
|
|
3315
3537
|
// CSWSH guard, before any per-connection work or the user's upgrade(): reject a disallowed
|
|
3316
3538
|
// Origin with 403. Browsers don't CORS-protect WS handshakes but do send cookies, so this
|
|
3317
3539
|
// blocks cross-site authenticated sockets when the route opts in via `allowedOrigins`.
|
|
@@ -3321,23 +3543,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3321
3543
|
typeof handler.allowedOrigins === "function"
|
|
3322
3544
|
? handler.allowedOrigins(origin)
|
|
3323
3545
|
: origin !== null && handler.allowedOrigins.includes(origin)
|
|
3324
|
-
if (!allowed) return
|
|
3546
|
+
if (!allowed) return refuse(jsonError(403, "forbidden_origin"))
|
|
3325
3547
|
} else if (origin !== null && !wsSameOrigin(origin, request)) {
|
|
3326
3548
|
// Secure default (no explicit `allowedOrigins`): reject a CROSS-ORIGIN browser handshake - the
|
|
3327
3549
|
// CSWSH case, since browsers send cookies on WS handshakes and don't apply CORS. Non-browser
|
|
3328
3550
|
// clients send no `Origin` and pass; same-origin browsers pass. Set `allowedOrigins` to permit
|
|
3329
3551
|
// specific cross-origin clients (or `() => true` for a genuinely public socket).
|
|
3330
|
-
return
|
|
3331
|
-
}
|
|
3332
|
-
if (handler.upgrade === undefined) {
|
|
3333
|
-
return {
|
|
3334
|
-
kind: "upgrade",
|
|
3335
|
-
handler,
|
|
3336
|
-
data: undefined,
|
|
3337
|
-
pubsub,
|
|
3338
|
-
attach,
|
|
3339
|
-
maxPayloadBytes: this.wsMaxPayloadBytes,
|
|
3340
|
-
}
|
|
3552
|
+
return refuse(jsonError(403, "forbidden_origin"))
|
|
3341
3553
|
}
|
|
3342
3554
|
const budget =
|
|
3343
3555
|
controller === undefined
|
|
@@ -3353,28 +3565,32 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3353
3565
|
this.maxBodyBytes,
|
|
3354
3566
|
this.protoPoisoning,
|
|
3355
3567
|
)
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3568
|
+
// Rendered as the same chain on an HTTP `GET` renders its early results.
|
|
3569
|
+
const finalize = (result: unknown, set: CtxSet): WebSocketUpgradeOutcome => {
|
|
3570
|
+
const accepted = runtime.accepted(result)
|
|
3571
|
+
return accepted === undefined
|
|
3572
|
+
? { kind: "reject", response: this.finalizeResponse(result, set) }
|
|
3359
3573
|
: {
|
|
3360
3574
|
kind: "upgrade",
|
|
3361
3575
|
handler,
|
|
3362
|
-
data:
|
|
3576
|
+
data: accepted.data,
|
|
3363
3577
|
pubsub,
|
|
3364
|
-
attach,
|
|
3578
|
+
attach: runtime.attach,
|
|
3365
3579
|
maxPayloadBytes: this.wsMaxPayloadBytes,
|
|
3580
|
+
sender: runtime.sender,
|
|
3366
3581
|
}
|
|
3582
|
+
}
|
|
3583
|
+
const wrap = (response: Response | ResponseResult): WebSocketUpgradeOutcome =>
|
|
3584
|
+
finalize(response, EMPTY_RESPONSE_CONTROLS)
|
|
3585
|
+
let outcome: MaybePromise<WebSocketUpgradeOutcome>
|
|
3367
3586
|
try {
|
|
3368
|
-
|
|
3369
|
-
if (!(result instanceof Promise)) return settle(result)
|
|
3370
|
-
const pending = result.then(settle, () => ({
|
|
3371
|
-
kind: "reject" as const,
|
|
3372
|
-
response: jsonError(500, "internal_error"),
|
|
3373
|
-
}))
|
|
3374
|
-
return bounded(pending, onTimeout)
|
|
3587
|
+
outcome = executeRouteProgram(this, chain, chain.program, request, ctx, finalize, wrap)
|
|
3375
3588
|
} catch {
|
|
3376
|
-
return
|
|
3589
|
+
return refuse(jsonError(500, "internal_error"))
|
|
3377
3590
|
}
|
|
3591
|
+
if (!(outcome instanceof Promise)) return outcome
|
|
3592
|
+
const pending = outcome.then(undefined, () => refuse(jsonError(500, "internal_error")))
|
|
3593
|
+
return bounded(pending, onTimeout)
|
|
3378
3594
|
}
|
|
3379
3595
|
|
|
3380
3596
|
const resolveMounted = (request: Request): MaybePromise<WebSocketUpgradeOutcome> => {
|
|
@@ -3397,20 +3613,25 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3397
3613
|
}
|
|
3398
3614
|
}
|
|
3399
3615
|
if (selected === undefined) return WS_PASS
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3616
|
+
let childRequest = request
|
|
3617
|
+
if (selected.stripPrefix) {
|
|
3618
|
+
childRequest = stripMountPrefix(request, selected.path)
|
|
3619
|
+
selected.wsPassed ??= new WeakMap()
|
|
3620
|
+
selected.wsPassed.set(request, childRequest)
|
|
3621
|
+
}
|
|
3403
3622
|
let child: MaybePromise<WebSocketUpgradeOutcome>
|
|
3404
3623
|
try {
|
|
3405
3624
|
child = selected.resolveWebSocketUpgrade!(childRequest, platform)
|
|
3406
3625
|
} catch {
|
|
3407
|
-
return
|
|
3626
|
+
return refuse(jsonError(500, "internal_error"))
|
|
3408
3627
|
}
|
|
3409
3628
|
const settle = (outcome: WebSocketUpgradeOutcome): MaybePromise<WebSocketUpgradeOutcome> =>
|
|
3410
3629
|
outcome.kind === "pass" ||
|
|
3411
3630
|
(outcome.kind === "reject" && outcome.response.status === 404 && selected!.fallbackOn404)
|
|
3412
3631
|
? dispatch(selectedIndex + 1)
|
|
3413
|
-
: outcome
|
|
3632
|
+
: outcome.kind === "reject"
|
|
3633
|
+
? refuse(outcome.response)
|
|
3634
|
+
: outcome
|
|
3414
3635
|
return child instanceof Promise ? child.then(settle) : settle(child)
|
|
3415
3636
|
}
|
|
3416
3637
|
return dispatch(0)
|
|
@@ -3432,23 +3653,25 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3432
3653
|
}
|
|
3433
3654
|
|
|
3434
3655
|
if (this.onRequestHooks.length === 0) return resolveWithDeadline(req)
|
|
3656
|
+
// On a pass the adapter hands `received` to `fetch`, which then routes these results. An upgrade
|
|
3657
|
+
// or a rejection never reaches `fetch`, so the entry dies with the request.
|
|
3658
|
+
const resolveHooked = (request: Request): MaybePromise<WebSocketUpgradeOutcome> => {
|
|
3659
|
+
this.wsHookedPasses ??= new WeakMap()
|
|
3660
|
+
this.wsHookedPasses.set(received, request)
|
|
3661
|
+
return resolveWithDeadline(request)
|
|
3662
|
+
}
|
|
3435
3663
|
let hooked: MaybePromise<Request | Response>
|
|
3436
3664
|
try {
|
|
3437
3665
|
hooked = this.runWebSocketRequestHooks(req, platform)
|
|
3438
3666
|
} catch {
|
|
3439
|
-
return
|
|
3667
|
+
return refuse(jsonError(500, "internal_error"))
|
|
3440
3668
|
}
|
|
3441
3669
|
if (!(hooked instanceof Promise)) {
|
|
3442
|
-
return hooked instanceof Response
|
|
3443
|
-
? { kind: "reject", response: hooked }
|
|
3444
|
-
: resolveWithDeadline(hooked)
|
|
3670
|
+
return hooked instanceof Response ? refuse(hooked) : resolveHooked(hooked)
|
|
3445
3671
|
}
|
|
3446
3672
|
const pending = hooked.then(
|
|
3447
|
-
(result) =>
|
|
3448
|
-
|
|
3449
|
-
? { kind: "reject" as const, response: result }
|
|
3450
|
-
: resolveWithDeadline(result),
|
|
3451
|
-
() => ({ kind: "reject" as const, response: jsonError(500, "internal_error") }),
|
|
3673
|
+
(result) => (result instanceof Response ? refuse(result) : resolveHooked(result)),
|
|
3674
|
+
() => refuse(jsonError(500, "internal_error")),
|
|
3452
3675
|
)
|
|
3453
3676
|
return bounded(
|
|
3454
3677
|
Promise.resolve(pending).then((result) => result),
|
|
@@ -3467,9 +3690,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3467
3690
|
if (o.kind === "pass")
|
|
3468
3691
|
return this.fetch(req, bunPeerPlatform(server, req) as Platform<EnvOf<Ctx>>)
|
|
3469
3692
|
if (o.kind === "reject") return o.response
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
})
|
|
3693
|
+
// A copy: the Bun socket keeps per-connection state on it, and a hand-built outcome may be shared.
|
|
3694
|
+
return server.upgrade(req, { data: { ...o } })
|
|
3473
3695
|
? undefined
|
|
3474
3696
|
: jsonError(426, "upgrade_required")
|
|
3475
3697
|
}
|
|
@@ -3512,7 +3734,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3512
3734
|
private dispatch<T>(
|
|
3513
3735
|
source: RequestSource,
|
|
3514
3736
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3515
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3737
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3516
3738
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3517
3739
|
onTimeout: () => T,
|
|
3518
3740
|
// True only from the Web `fetch` path - unlocks each route's fused lane, whose output type IS
|
|
@@ -3532,6 +3754,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3532
3754
|
if (this.onRequestHooks.length === 0 || !isRoutableMethod(source.method)) {
|
|
3533
3755
|
return this.routeAndRun(source, resolved, finalize, wrapResponse, onTimeout, webFast)
|
|
3534
3756
|
}
|
|
3757
|
+
const hooked = this.wsHookedPasses?.get(source)
|
|
3758
|
+
if (hooked !== undefined) {
|
|
3759
|
+
const request = requestOf(source)
|
|
3760
|
+
this.wsHookedPasses?.delete(request)
|
|
3761
|
+
this.responseRequests.set(request, hooked)
|
|
3762
|
+
return this.routeAndRun(hooked, resolved, finalize, wrapResponse, onTimeout, webFast)
|
|
3763
|
+
}
|
|
3535
3764
|
if (!webFast && this.canUseNodeRequestHooks()) {
|
|
3536
3765
|
return this.runWithNodeRequest(source, resolved, finalize, wrapResponse, onTimeout)
|
|
3537
3766
|
}
|
|
@@ -3543,7 +3772,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3543
3772
|
private runWithNodeRequest<T>(
|
|
3544
3773
|
source: RequestSource,
|
|
3545
3774
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3546
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3775
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3547
3776
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3548
3777
|
onTimeout: () => T,
|
|
3549
3778
|
): MaybePromise<T> {
|
|
@@ -3577,7 +3806,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3577
3806
|
request: NodeRequestContext,
|
|
3578
3807
|
source: RequestSource,
|
|
3579
3808
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3580
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3809
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3581
3810
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3582
3811
|
onTimeout: () => T,
|
|
3583
3812
|
): Promise<T> {
|
|
@@ -3605,7 +3834,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3605
3834
|
private runWithOnRequest<T>(
|
|
3606
3835
|
source: RequestSource,
|
|
3607
3836
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3608
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3837
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3609
3838
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3610
3839
|
onTimeout: () => T,
|
|
3611
3840
|
webFast: boolean,
|
|
@@ -3654,7 +3883,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3654
3883
|
originalRequest: Request,
|
|
3655
3884
|
sourceAtAwait: RequestSource,
|
|
3656
3885
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3657
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3886
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3658
3887
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3659
3888
|
onTimeout: () => T,
|
|
3660
3889
|
webFast: boolean,
|
|
@@ -3751,7 +3980,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3751
3980
|
private routeAndRun<T>(
|
|
3752
3981
|
source: RequestSource,
|
|
3753
3982
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3754
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3983
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3755
3984
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3756
3985
|
onTimeout: () => T,
|
|
3757
3986
|
webFast: boolean,
|
|
@@ -3881,7 +4110,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3881
4110
|
}
|
|
3882
4111
|
}
|
|
3883
4112
|
if (mount === undefined) return fallback ?? jsonError(404, "not_found")
|
|
3884
|
-
const childRequest = mount.stripPrefix
|
|
4113
|
+
const childRequest = mount.stripPrefix
|
|
4114
|
+
? (mount.wsPassed?.get(request) ?? stripMountPrefix(request, mount.path))
|
|
4115
|
+
: request
|
|
3885
4116
|
const outcome = mount.handler(childRequest, platform)
|
|
3886
4117
|
if (!replayable || !mount.fallbackOn404) return outcome
|
|
3887
4118
|
if (outcome instanceof Promise) {
|
|
@@ -3901,7 +4132,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3901
4132
|
entry: RouteEntry,
|
|
3902
4133
|
params: Record<string, string>,
|
|
3903
4134
|
search: string | undefined,
|
|
3904
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4135
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3905
4136
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3906
4137
|
onTimeout: () => T,
|
|
3907
4138
|
webFast: boolean,
|
|
@@ -3939,7 +4170,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3939
4170
|
{
|
|
3940
4171
|
maxBodyBytes: entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3941
4172
|
runLanes: (buffered, plat, ent, prm, srch) =>
|
|
3942
|
-
this.
|
|
4173
|
+
this.runMatchedLanes(
|
|
4174
|
+
buffered,
|
|
4175
|
+
plat,
|
|
4176
|
+
ent as RouteEntry,
|
|
4177
|
+
prm,
|
|
4178
|
+
srch,
|
|
4179
|
+
this.finalizeResponse,
|
|
4180
|
+
this.wrapWebResponse,
|
|
4181
|
+
this.webResponseTimeout,
|
|
4182
|
+
false,
|
|
4183
|
+
),
|
|
3943
4184
|
},
|
|
3944
4185
|
)
|
|
3945
4186
|
}
|
|
@@ -3963,7 +4204,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3963
4204
|
entry: RouteEntry,
|
|
3964
4205
|
params: Record<string, string>,
|
|
3965
4206
|
search: string | undefined,
|
|
3966
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4207
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3967
4208
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3968
4209
|
onTimeout: () => T,
|
|
3969
4210
|
webFast: boolean,
|
|
@@ -4004,32 +4245,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4004
4245
|
: createRequestBudget({ deadline: admission!.deadline as number, signal })
|
|
4005
4246
|
const plan = entry.execution
|
|
4006
4247
|
const nativeContext = controller === undefined
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
search,
|
|
4013
|
-
signal,
|
|
4014
|
-
budget,
|
|
4015
|
-
platform,
|
|
4016
|
-
nativeContext,
|
|
4017
|
-
) as MaybePromise<T>)
|
|
4018
|
-
: !webFast && plan.fusedBody !== undefined
|
|
4019
|
-
? plan.fusedBody(
|
|
4020
|
-
source,
|
|
4021
|
-
params,
|
|
4022
|
-
search,
|
|
4023
|
-
signal,
|
|
4024
|
-
budget,
|
|
4025
|
-
platform,
|
|
4026
|
-
false,
|
|
4027
|
-
finalize,
|
|
4028
|
-
wrapResponse,
|
|
4029
|
-
)
|
|
4030
|
-
: plan.run(
|
|
4031
|
-
this,
|
|
4032
|
-
entry,
|
|
4248
|
+
let outcome: MaybePromise<T>
|
|
4249
|
+
try {
|
|
4250
|
+
outcome =
|
|
4251
|
+
webFast && plan.fusedWeb !== undefined
|
|
4252
|
+
? (plan.fusedWeb(
|
|
4033
4253
|
source,
|
|
4034
4254
|
params,
|
|
4035
4255
|
search,
|
|
@@ -4037,9 +4257,46 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4037
4257
|
budget,
|
|
4038
4258
|
platform,
|
|
4039
4259
|
nativeContext,
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4260
|
+
) as MaybePromise<T>)
|
|
4261
|
+
: !webFast && plan.fusedBody !== undefined
|
|
4262
|
+
? plan.fusedBody(
|
|
4263
|
+
source,
|
|
4264
|
+
params,
|
|
4265
|
+
search,
|
|
4266
|
+
signal,
|
|
4267
|
+
budget,
|
|
4268
|
+
platform,
|
|
4269
|
+
false,
|
|
4270
|
+
finalize,
|
|
4271
|
+
wrapResponse,
|
|
4272
|
+
)
|
|
4273
|
+
: plan.run(
|
|
4274
|
+
this,
|
|
4275
|
+
entry,
|
|
4276
|
+
source,
|
|
4277
|
+
params,
|
|
4278
|
+
search,
|
|
4279
|
+
signal,
|
|
4280
|
+
budget,
|
|
4281
|
+
platform,
|
|
4282
|
+
nativeContext,
|
|
4283
|
+
finalize,
|
|
4284
|
+
wrapResponse,
|
|
4285
|
+
)
|
|
4286
|
+
} catch (err) {
|
|
4287
|
+
// A lane's synchronous render throw (a BigInt, a header value with a line break): the lanes
|
|
4288
|
+
// guard only their async continuations, since a `try` around a fused render slows it.
|
|
4289
|
+
return this.contextlessBareError(
|
|
4290
|
+
err,
|
|
4291
|
+
source,
|
|
4292
|
+
params,
|
|
4293
|
+
search,
|
|
4294
|
+
signal,
|
|
4295
|
+
budget,
|
|
4296
|
+
platform,
|
|
4297
|
+
wrapResponse,
|
|
4298
|
+
)
|
|
4299
|
+
}
|
|
4043
4300
|
// The request timeout only bounds work that is actually pending - a synchronous (bare) result is
|
|
4044
4301
|
// already complete and can't time out, so it's returned as-is (no 503 race, no promise).
|
|
4045
4302
|
if (controller !== undefined && outcome instanceof Promise) {
|
|
@@ -4074,7 +4331,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4074
4331
|
signal: AbortSignal,
|
|
4075
4332
|
budget: RequestBudget,
|
|
4076
4333
|
platform: Platform | undefined,
|
|
4077
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4334
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
4078
4335
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4079
4336
|
): MaybePromise<T> {
|
|
4080
4337
|
let result: unknown
|
|
@@ -4094,7 +4351,22 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4094
4351
|
}
|
|
4095
4352
|
if (result instanceof Promise) {
|
|
4096
4353
|
return result.then(
|
|
4097
|
-
(value) =>
|
|
4354
|
+
(value) => {
|
|
4355
|
+
try {
|
|
4356
|
+
return finalize(value, EMPTY_RESPONSE_CONTROLS)
|
|
4357
|
+
} catch (err) {
|
|
4358
|
+
return this.contextlessBareError(
|
|
4359
|
+
err,
|
|
4360
|
+
source,
|
|
4361
|
+
params,
|
|
4362
|
+
search,
|
|
4363
|
+
signal,
|
|
4364
|
+
budget,
|
|
4365
|
+
platform,
|
|
4366
|
+
wrapResponse,
|
|
4367
|
+
)
|
|
4368
|
+
}
|
|
4369
|
+
},
|
|
4098
4370
|
(err) =>
|
|
4099
4371
|
this.contextlessBareError(
|
|
4100
4372
|
err,
|
|
@@ -4151,7 +4423,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4151
4423
|
private runBare<T>(
|
|
4152
4424
|
entry: RouteEntry,
|
|
4153
4425
|
ctx: RawContext,
|
|
4154
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4426
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
4155
4427
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4156
4428
|
): MaybePromise<T> {
|
|
4157
4429
|
let result: unknown
|
|
@@ -4164,11 +4436,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4164
4436
|
if (result instanceof Promise) {
|
|
4165
4437
|
// Async handler on an otherwise-bare route: finish on a microtask, with the same error handling.
|
|
4166
4438
|
return result.then(
|
|
4167
|
-
(value) =>
|
|
4439
|
+
(value) => {
|
|
4440
|
+
// A synchronous finalize throw reaches runMatchedLanes; this one would reject.
|
|
4441
|
+
try {
|
|
4442
|
+
return finalize(value, responseSet(ctx), ctx)
|
|
4443
|
+
} catch (err) {
|
|
4444
|
+
return this.bareError(err, ctx, finalize, wrapResponse)
|
|
4445
|
+
}
|
|
4446
|
+
},
|
|
4168
4447
|
(err) => this.bareError(err, ctx, finalize, wrapResponse),
|
|
4169
4448
|
)
|
|
4170
4449
|
}
|
|
4171
|
-
return finalize(result, responseSet(ctx))
|
|
4450
|
+
return finalize(result, responseSet(ctx), ctx)
|
|
4172
4451
|
}
|
|
4173
4452
|
|
|
4174
4453
|
/** Bare-route error rendering - identical to the compiled route program's catch minus the (absent) onError
|
|
@@ -4200,66 +4479,62 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4200
4479
|
// `() => ...` can't observe the context - skip allocating one entirely (errors still build
|
|
4201
4480
|
// one for the structured log, exactly like runContextlessBare).
|
|
4202
4481
|
const contextlessHandler = handler as unknown as ContextlessHandler
|
|
4482
|
+
const fail = (
|
|
4483
|
+
err: unknown,
|
|
4484
|
+
source: RequestSource,
|
|
4485
|
+
params: Record<string, string>,
|
|
4486
|
+
search: string | undefined,
|
|
4487
|
+
signal: AbortSignal,
|
|
4488
|
+
budget: RequestBudget,
|
|
4489
|
+
platform: Platform | undefined,
|
|
4490
|
+
nativeContext: boolean,
|
|
4491
|
+
): Response =>
|
|
4492
|
+
logError(
|
|
4493
|
+
err,
|
|
4494
|
+
nativeContext
|
|
4495
|
+
? RequestContext.native(
|
|
4496
|
+
source,
|
|
4497
|
+
params,
|
|
4498
|
+
search,
|
|
4499
|
+
maxBodyBytes,
|
|
4500
|
+
platform,
|
|
4501
|
+
this.protoPoisoning,
|
|
4502
|
+
)
|
|
4503
|
+
: new RequestContext(
|
|
4504
|
+
source,
|
|
4505
|
+
params,
|
|
4506
|
+
search,
|
|
4507
|
+
signal,
|
|
4508
|
+
budget,
|
|
4509
|
+
platform,
|
|
4510
|
+
maxBodyBytes,
|
|
4511
|
+
this.protoPoisoning,
|
|
4512
|
+
),
|
|
4513
|
+
)
|
|
4203
4514
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
4204
4515
|
let result: unknown
|
|
4205
4516
|
try {
|
|
4206
4517
|
result = contextlessHandler()
|
|
4207
4518
|
} catch (err) {
|
|
4208
|
-
return
|
|
4209
|
-
err,
|
|
4210
|
-
nativeContext
|
|
4211
|
-
? RequestContext.native(
|
|
4212
|
-
source,
|
|
4213
|
-
params,
|
|
4214
|
-
search,
|
|
4215
|
-
maxBodyBytes,
|
|
4216
|
-
platform,
|
|
4217
|
-
this.protoPoisoning,
|
|
4218
|
-
)
|
|
4219
|
-
: new RequestContext(
|
|
4220
|
-
source,
|
|
4221
|
-
params,
|
|
4222
|
-
search,
|
|
4223
|
-
signal,
|
|
4224
|
-
budget,
|
|
4225
|
-
platform,
|
|
4226
|
-
maxBodyBytes,
|
|
4227
|
-
this.protoPoisoning,
|
|
4228
|
-
),
|
|
4229
|
-
)
|
|
4519
|
+
return fail(err, source, params, search, signal, budget, platform, nativeContext)
|
|
4230
4520
|
}
|
|
4231
4521
|
if (result instanceof Promise) {
|
|
4232
4522
|
return result.then(
|
|
4233
|
-
(value) =>
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
maxBodyBytes,
|
|
4249
|
-
platform,
|
|
4250
|
-
this.protoPoisoning,
|
|
4251
|
-
)
|
|
4252
|
-
: new RequestContext(
|
|
4253
|
-
source,
|
|
4254
|
-
params,
|
|
4255
|
-
search,
|
|
4256
|
-
signal,
|
|
4257
|
-
budget,
|
|
4258
|
-
platform,
|
|
4259
|
-
maxBodyBytes,
|
|
4260
|
-
this.protoPoisoning,
|
|
4261
|
-
),
|
|
4262
|
-
),
|
|
4523
|
+
(value) => {
|
|
4524
|
+
// A synchronous render throw reaches runMatchedLanes; this one would reject.
|
|
4525
|
+
try {
|
|
4526
|
+
return fusedRespondNoSet(
|
|
4527
|
+
value,
|
|
4528
|
+
this.responseBodyTag,
|
|
4529
|
+
this.staticResponseHeaders,
|
|
4530
|
+
this.onResponseHooks.length === 0,
|
|
4531
|
+
this.onResponseHooks.length === 0 ? undefined : requestOf(source),
|
|
4532
|
+
)
|
|
4533
|
+
} catch (err) {
|
|
4534
|
+
return fail(err, source, params, search, signal, budget, platform, nativeContext)
|
|
4535
|
+
}
|
|
4536
|
+
},
|
|
4537
|
+
(err) => fail(err, source, params, search, signal, budget, platform, nativeContext),
|
|
4263
4538
|
)
|
|
4264
4539
|
}
|
|
4265
4540
|
return fusedRespondNoSet(
|
|
@@ -4267,6 +4542,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4267
4542
|
this.responseBodyTag,
|
|
4268
4543
|
this.staticResponseHeaders,
|
|
4269
4544
|
this.onResponseHooks.length === 0,
|
|
4545
|
+
this.onResponseHooks.length === 0 ? undefined : requestOf(source),
|
|
4270
4546
|
)
|
|
4271
4547
|
}
|
|
4272
4548
|
}
|
|
@@ -4292,14 +4568,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4292
4568
|
}
|
|
4293
4569
|
if (result instanceof Promise) {
|
|
4294
4570
|
return result.then(
|
|
4295
|
-
(value) =>
|
|
4296
|
-
fusedRespond(
|
|
4297
|
-
value,
|
|
4298
|
-
ctx,
|
|
4299
|
-
this.responseBodyTag,
|
|
4300
|
-
this.staticResponseHeaders,
|
|
4301
|
-
this.onResponseHooks.length === 0,
|
|
4302
|
-
),
|
|
4571
|
+
(value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0),
|
|
4303
4572
|
(err) => logError(err, ctx),
|
|
4304
4573
|
)
|
|
4305
4574
|
}
|
|
@@ -4313,6 +4582,21 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4313
4582
|
}
|
|
4314
4583
|
}
|
|
4315
4584
|
|
|
4585
|
+
/** A fused Web lane's render in an async continuation, where a result that cannot be rendered
|
|
4586
|
+
* must fail like a handler throw; a synchronous render throw reaches {@link runMatchedLanes}. */
|
|
4587
|
+
private respondFused(result: unknown, ctx: RawContext, unstamped?: boolean): Response {
|
|
4588
|
+
try {
|
|
4589
|
+
return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, unstamped)
|
|
4590
|
+
} catch (err) {
|
|
4591
|
+
return this.bareError(
|
|
4592
|
+
err,
|
|
4593
|
+
ctx,
|
|
4594
|
+
(value, set) => this.wrapWebResponse(toResponse(value, set)),
|
|
4595
|
+
this.wrapWebResponse,
|
|
4596
|
+
)
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4316
4600
|
/** Compile the eligible body-only route's parser → validator → handler continuation once. The
|
|
4317
4601
|
* bounded parser remains shared with the generic lane; only the route-invariant entry lookups and
|
|
4318
4602
|
* lifecycle dispatch disappear from the common synchronous-validator/synchronous-handler case. */
|
|
@@ -4435,6 +4719,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4435
4719
|
wrapResponse,
|
|
4436
4720
|
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
4437
4721
|
bodySchema,
|
|
4722
|
+
this.formOrigins,
|
|
4438
4723
|
)
|
|
4439
4724
|
}
|
|
4440
4725
|
}
|
|
@@ -4496,24 +4781,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4496
4781
|
}
|
|
4497
4782
|
if (result instanceof Promise) {
|
|
4498
4783
|
return result.then(
|
|
4499
|
-
(settled) =>
|
|
4500
|
-
fusedRespond(
|
|
4501
|
-
settled,
|
|
4502
|
-
ctx,
|
|
4503
|
-
this.responseBodyTag,
|
|
4504
|
-
this.staticResponseHeaders,
|
|
4505
|
-
this.onResponseHooks.length === 0,
|
|
4506
|
-
),
|
|
4784
|
+
(settled) => this.respondFused(settled, ctx, this.onResponseHooks.length === 0),
|
|
4507
4785
|
(err) => logError(err, ctx),
|
|
4508
4786
|
)
|
|
4509
4787
|
}
|
|
4510
|
-
return
|
|
4511
|
-
result,
|
|
4512
|
-
ctx,
|
|
4513
|
-
this.responseBodyTag,
|
|
4514
|
-
this.staticResponseHeaders,
|
|
4515
|
-
this.onResponseHooks.length === 0,
|
|
4516
|
-
)
|
|
4788
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
4517
4789
|
}
|
|
4518
4790
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
4519
4791
|
const ctx = nativeContext
|
|
@@ -4572,6 +4844,25 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4572
4844
|
before: RawBeforeHandle,
|
|
4573
4845
|
querySchema: StandardSchemaV1 | undefined,
|
|
4574
4846
|
maxBodyBytes: number,
|
|
4847
|
+
): FusedWebRunner {
|
|
4848
|
+
return this.buildFusedDeriveBeforeAfter(
|
|
4849
|
+
handler,
|
|
4850
|
+
derive,
|
|
4851
|
+
before,
|
|
4852
|
+
undefined,
|
|
4853
|
+
querySchema,
|
|
4854
|
+
maxBodyBytes,
|
|
4855
|
+
)
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
// Both hook variants share one registered chain; request contexts stay in arguments.
|
|
4859
|
+
private buildFusedDeriveBeforeAfter(
|
|
4860
|
+
handler: InternalHandler,
|
|
4861
|
+
derive: RawDerive,
|
|
4862
|
+
before: RawBeforeHandle,
|
|
4863
|
+
after: RawAfterHandle | undefined,
|
|
4864
|
+
querySchema: StandardSchemaV1 | undefined,
|
|
4865
|
+
maxBodyBytes: number,
|
|
4575
4866
|
): FusedWebRunner {
|
|
4576
4867
|
const logError = (err: unknown, ctx: RawContext): Response =>
|
|
4577
4868
|
this.bareError(
|
|
@@ -4580,139 +4871,83 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4580
4871
|
(result, set) => this.wrapWebResponse(toResponse(result, set)),
|
|
4581
4872
|
this.wrapWebResponse,
|
|
4582
4873
|
)
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
)
|
|
4596
|
-
|
|
4597
|
-
let result: unknown
|
|
4598
|
-
try {
|
|
4599
|
-
result = handler(ctx)
|
|
4600
|
-
} catch (err) {
|
|
4601
|
-
return logError(err, ctx)
|
|
4602
|
-
}
|
|
4603
|
-
if (result instanceof Promise) {
|
|
4604
|
-
return result.then(
|
|
4605
|
-
(value) =>
|
|
4606
|
-
fusedRespond(
|
|
4607
|
-
value,
|
|
4608
|
-
ctx,
|
|
4609
|
-
this.responseBodyTag,
|
|
4610
|
-
this.staticResponseHeaders,
|
|
4611
|
-
this.onResponseHooks.length === 0,
|
|
4612
|
-
),
|
|
4613
|
-
(err) => logError(err, ctx),
|
|
4614
|
-
)
|
|
4615
|
-
}
|
|
4616
|
-
return fusedRespond(
|
|
4617
|
-
result,
|
|
4618
|
-
ctx,
|
|
4619
|
-
this.responseBodyTag,
|
|
4620
|
-
this.staticResponseHeaders,
|
|
4621
|
-
this.onResponseHooks.length === 0,
|
|
4874
|
+
// Continuations capture route state once; each request supplies its own context.
|
|
4875
|
+
const runAfter = (result: unknown, ctx: RawContext): MaybePromise<Response> => {
|
|
4876
|
+
if (after === undefined)
|
|
4877
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
4878
|
+
let transformed: unknown
|
|
4879
|
+
try {
|
|
4880
|
+
transformed = after(result, ctx)
|
|
4881
|
+
} catch (err) {
|
|
4882
|
+
return logError(err, ctx)
|
|
4883
|
+
}
|
|
4884
|
+
if (transformed instanceof Promise) {
|
|
4885
|
+
return transformed.then(
|
|
4886
|
+
(value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0),
|
|
4887
|
+
(err) => logError(err, ctx),
|
|
4622
4888
|
)
|
|
4623
4889
|
}
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
return early.then(
|
|
4633
|
-
(settled) => {
|
|
4634
|
-
if (settled === undefined) return runHandler()
|
|
4635
|
-
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4636
|
-
},
|
|
4637
|
-
(err) => logError(err, ctx),
|
|
4638
|
-
)
|
|
4639
|
-
}
|
|
4640
|
-
if (early === undefined) return runHandler()
|
|
4641
|
-
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4890
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0)
|
|
4891
|
+
}
|
|
4892
|
+
const runHandler = (ctx: RawContext): MaybePromise<Response> => {
|
|
4893
|
+
let result: unknown
|
|
4894
|
+
try {
|
|
4895
|
+
result = handler(ctx)
|
|
4896
|
+
} catch (err) {
|
|
4897
|
+
return logError(err, ctx)
|
|
4642
4898
|
}
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
return logError(err, ctx)
|
|
4649
|
-
}
|
|
4650
|
-
if (derived instanceof Promise) {
|
|
4651
|
-
return derived.then(
|
|
4652
|
-
(settled) => {
|
|
4653
|
-
if (isResponseResult(settled) || settled instanceof Response) {
|
|
4654
|
-
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4655
|
-
}
|
|
4656
|
-
Object.assign(ctx, settled as object)
|
|
4657
|
-
return runBefore()
|
|
4658
|
-
},
|
|
4659
|
-
(err) => logError(err, ctx),
|
|
4660
|
-
)
|
|
4661
|
-
}
|
|
4662
|
-
if (isResponseResult(derived) || derived instanceof Response) {
|
|
4663
|
-
return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4664
|
-
}
|
|
4665
|
-
Object.assign(ctx, derived as object)
|
|
4666
|
-
return runBefore()
|
|
4899
|
+
if (result instanceof Promise) {
|
|
4900
|
+
return result.then(
|
|
4901
|
+
(value) => runAfter(value, ctx),
|
|
4902
|
+
(err) => logError(err, ctx),
|
|
4903
|
+
)
|
|
4667
4904
|
}
|
|
4668
|
-
|
|
4669
|
-
|
|
4905
|
+
return runAfter(result, ctx)
|
|
4906
|
+
}
|
|
4907
|
+
const runBefore = (ctx: RawContext): MaybePromise<Response> => {
|
|
4908
|
+
let early: unknown
|
|
4670
4909
|
try {
|
|
4671
|
-
|
|
4910
|
+
early = before(ctx)
|
|
4672
4911
|
} catch (err) {
|
|
4673
4912
|
return logError(err, ctx)
|
|
4674
4913
|
}
|
|
4675
|
-
if (
|
|
4676
|
-
return
|
|
4914
|
+
if (early instanceof Promise) {
|
|
4915
|
+
return early.then(
|
|
4677
4916
|
(settled) => {
|
|
4678
|
-
if (settled
|
|
4679
|
-
|
|
4680
|
-
ctx.query = settled.value
|
|
4681
|
-
return runDerive()
|
|
4917
|
+
if (settled === undefined) return runHandler(ctx)
|
|
4918
|
+
return this.respondFused(settled, ctx)
|
|
4682
4919
|
},
|
|
4683
4920
|
(err) => logError(err, ctx),
|
|
4684
4921
|
)
|
|
4685
4922
|
}
|
|
4686
|
-
if (
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4923
|
+
if (early === undefined) return runHandler(ctx)
|
|
4924
|
+
return this.respondFused(early, ctx)
|
|
4925
|
+
}
|
|
4926
|
+
const runDerive = (ctx: RawContext): MaybePromise<Response> => {
|
|
4927
|
+
let derived: unknown
|
|
4928
|
+
try {
|
|
4929
|
+
derived = derive(ctx)
|
|
4930
|
+
} catch (err) {
|
|
4931
|
+
return logError(err, ctx)
|
|
4932
|
+
}
|
|
4933
|
+
if (derived instanceof Promise) {
|
|
4934
|
+
return derived.then(
|
|
4935
|
+
(settled) => {
|
|
4936
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
4937
|
+
return this.respondFused(settled, ctx)
|
|
4938
|
+
}
|
|
4939
|
+
Object.assign(ctx, settled as object)
|
|
4940
|
+
return runBefore(ctx)
|
|
4941
|
+
},
|
|
4942
|
+
(err) => logError(err, ctx),
|
|
4943
|
+
)
|
|
4944
|
+
}
|
|
4945
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
4946
|
+
return this.respondFused(derived, ctx)
|
|
4947
|
+
}
|
|
4948
|
+
Object.assign(ctx, derived as object)
|
|
4949
|
+
return runBefore(ctx)
|
|
4690
4950
|
}
|
|
4691
|
-
}
|
|
4692
|
-
|
|
4693
|
-
/**
|
|
4694
|
-
* Same shape as `buildFusedDeriveBefore`, plus one `afterHandle` step. The closure runs (validate) →
|
|
4695
|
-
* derive → before → handler → after in one frame, the shape the realistic middleware route lands on
|
|
4696
|
-
* when it also wants a response transform (e.g. attaching a request id to a successful payload's
|
|
4697
|
-
* headers). `LifecycleExecutionLane` and `selectRouteLanes` guarantee `afterHandle === 1`, the
|
|
4698
|
-
* throw contract is the same as the generic program, and the `after` step is the final transform +
|
|
4699
|
-
* respond the generic program already does.
|
|
4700
|
-
*/
|
|
4701
|
-
private buildFusedDeriveBeforeAfter(
|
|
4702
|
-
handler: InternalHandler,
|
|
4703
|
-
derive: RawDerive,
|
|
4704
|
-
before: RawBeforeHandle,
|
|
4705
|
-
after: RawAfterHandle,
|
|
4706
|
-
querySchema: StandardSchemaV1 | undefined,
|
|
4707
|
-
maxBodyBytes: number,
|
|
4708
|
-
): FusedWebRunner {
|
|
4709
|
-
const logError = (err: unknown, ctx: RawContext): Response =>
|
|
4710
|
-
this.bareError(
|
|
4711
|
-
err,
|
|
4712
|
-
ctx,
|
|
4713
|
-
(result, set) => this.wrapWebResponse(toResponse(result, set)),
|
|
4714
|
-
this.wrapWebResponse,
|
|
4715
|
-
)
|
|
4716
4951
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
4717
4952
|
const ctx = nativeContext
|
|
4718
4953
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
@@ -4726,94 +4961,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4726
4961
|
maxBodyBytes,
|
|
4727
4962
|
this.protoPoisoning,
|
|
4728
4963
|
)
|
|
4729
|
-
|
|
4730
|
-
let transformed: unknown
|
|
4731
|
-
try {
|
|
4732
|
-
transformed = after(result, ctx)
|
|
4733
|
-
} catch (err) {
|
|
4734
|
-
return logError(err, ctx)
|
|
4735
|
-
}
|
|
4736
|
-
if (transformed instanceof Promise) {
|
|
4737
|
-
return transformed.then(
|
|
4738
|
-
(value) =>
|
|
4739
|
-
fusedRespond(
|
|
4740
|
-
value,
|
|
4741
|
-
ctx,
|
|
4742
|
-
this.responseBodyTag,
|
|
4743
|
-
this.staticResponseHeaders,
|
|
4744
|
-
this.onResponseHooks.length === 0,
|
|
4745
|
-
),
|
|
4746
|
-
(err) => logError(err, ctx),
|
|
4747
|
-
)
|
|
4748
|
-
}
|
|
4749
|
-
return fusedRespond(
|
|
4750
|
-
transformed,
|
|
4751
|
-
ctx,
|
|
4752
|
-
this.responseBodyTag,
|
|
4753
|
-
this.staticResponseHeaders,
|
|
4754
|
-
this.onResponseHooks.length === 0,
|
|
4755
|
-
)
|
|
4756
|
-
}
|
|
4757
|
-
const runHandler = (): MaybePromise<Response> => {
|
|
4758
|
-
let result: unknown
|
|
4759
|
-
try {
|
|
4760
|
-
result = handler(ctx)
|
|
4761
|
-
} catch (err) {
|
|
4762
|
-
return logError(err, ctx)
|
|
4763
|
-
}
|
|
4764
|
-
if (result instanceof Promise) {
|
|
4765
|
-
return result.then(
|
|
4766
|
-
(value) => runAfter(value),
|
|
4767
|
-
(err) => logError(err, ctx),
|
|
4768
|
-
)
|
|
4769
|
-
}
|
|
4770
|
-
return runAfter(result)
|
|
4771
|
-
}
|
|
4772
|
-
const runBefore = (): MaybePromise<Response> => {
|
|
4773
|
-
let early: unknown
|
|
4774
|
-
try {
|
|
4775
|
-
early = before(ctx)
|
|
4776
|
-
} catch (err) {
|
|
4777
|
-
return logError(err, ctx)
|
|
4778
|
-
}
|
|
4779
|
-
if (early instanceof Promise) {
|
|
4780
|
-
return early.then(
|
|
4781
|
-
(settled) => {
|
|
4782
|
-
if (settled === undefined) return runHandler()
|
|
4783
|
-
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4784
|
-
},
|
|
4785
|
-
(err) => logError(err, ctx),
|
|
4786
|
-
)
|
|
4787
|
-
}
|
|
4788
|
-
if (early === undefined) return runHandler()
|
|
4789
|
-
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4790
|
-
}
|
|
4791
|
-
const runDerive = (): MaybePromise<Response> => {
|
|
4792
|
-
let derived: unknown
|
|
4793
|
-
try {
|
|
4794
|
-
derived = derive(ctx)
|
|
4795
|
-
} catch (err) {
|
|
4796
|
-
return logError(err, ctx)
|
|
4797
|
-
}
|
|
4798
|
-
if (derived instanceof Promise) {
|
|
4799
|
-
return derived.then(
|
|
4800
|
-
(settled) => {
|
|
4801
|
-
if (isResponseResult(settled) || settled instanceof Response) {
|
|
4802
|
-
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4803
|
-
}
|
|
4804
|
-
Object.assign(ctx, settled as object)
|
|
4805
|
-
return runBefore()
|
|
4806
|
-
},
|
|
4807
|
-
(err) => logError(err, ctx),
|
|
4808
|
-
)
|
|
4809
|
-
}
|
|
4810
|
-
if (isResponseResult(derived) || derived instanceof Response) {
|
|
4811
|
-
return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders)
|
|
4812
|
-
}
|
|
4813
|
-
Object.assign(ctx, derived as object)
|
|
4814
|
-
return runBefore()
|
|
4815
|
-
}
|
|
4816
|
-
if (querySchema === undefined) return runDerive()
|
|
4964
|
+
if (querySchema === undefined) return runDerive(ctx)
|
|
4817
4965
|
let validation: MaybePromise<StandardResult<unknown>>
|
|
4818
4966
|
try {
|
|
4819
4967
|
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]))
|
|
@@ -4826,7 +4974,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4826
4974
|
if (settled.issues !== undefined)
|
|
4827
4975
|
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
4828
4976
|
ctx.query = settled.value
|
|
4829
|
-
return runDerive()
|
|
4977
|
+
return runDerive(ctx)
|
|
4830
4978
|
},
|
|
4831
4979
|
(err) => logError(err, ctx),
|
|
4832
4980
|
)
|
|
@@ -4834,7 +4982,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4834
4982
|
if (validation.issues !== undefined)
|
|
4835
4983
|
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
4836
4984
|
ctx.query = validation.value
|
|
4837
|
-
return runDerive()
|
|
4985
|
+
return runDerive(ctx)
|
|
4838
4986
|
}
|
|
4839
4987
|
}
|
|
4840
4988
|
|
|
@@ -4881,13 +5029,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4881
5029
|
const runHandlerValidated = (): MaybePromise<Response> => {
|
|
4882
5030
|
const runAfter = (result: unknown): MaybePromise<Response> => {
|
|
4883
5031
|
if (after === undefined) {
|
|
4884
|
-
return
|
|
4885
|
-
result,
|
|
4886
|
-
ctx,
|
|
4887
|
-
this.responseBodyTag,
|
|
4888
|
-
this.staticResponseHeaders,
|
|
4889
|
-
this.onResponseHooks.length === 0,
|
|
4890
|
-
)
|
|
5032
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
4891
5033
|
}
|
|
4892
5034
|
let transformed: unknown
|
|
4893
5035
|
try {
|
|
@@ -4897,24 +5039,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4897
5039
|
}
|
|
4898
5040
|
if (transformed instanceof Promise) {
|
|
4899
5041
|
return transformed.then(
|
|
4900
|
-
(value) =>
|
|
4901
|
-
fusedRespond(
|
|
4902
|
-
value,
|
|
4903
|
-
ctx,
|
|
4904
|
-
this.responseBodyTag,
|
|
4905
|
-
this.staticResponseHeaders,
|
|
4906
|
-
this.onResponseHooks.length === 0,
|
|
4907
|
-
),
|
|
5042
|
+
(value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0),
|
|
4908
5043
|
(err) => logError(err, ctx),
|
|
4909
5044
|
)
|
|
4910
5045
|
}
|
|
4911
|
-
return
|
|
4912
|
-
transformed,
|
|
4913
|
-
ctx,
|
|
4914
|
-
this.responseBodyTag,
|
|
4915
|
-
this.staticResponseHeaders,
|
|
4916
|
-
this.onResponseHooks.length === 0,
|
|
4917
|
-
)
|
|
5046
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0)
|
|
4918
5047
|
}
|
|
4919
5048
|
const runHandler = (): MaybePromise<Response> => {
|
|
4920
5049
|
let result: unknown
|
|
@@ -4942,13 +5071,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4942
5071
|
return early.then(
|
|
4943
5072
|
(settled) => {
|
|
4944
5073
|
if (settled === undefined) return runHandler()
|
|
4945
|
-
return
|
|
5074
|
+
return this.respondFused(settled, ctx)
|
|
4946
5075
|
},
|
|
4947
5076
|
(err) => logError(err, ctx),
|
|
4948
5077
|
)
|
|
4949
5078
|
}
|
|
4950
5079
|
if (early === undefined) return runHandler()
|
|
4951
|
-
return
|
|
5080
|
+
return this.respondFused(early, ctx)
|
|
4952
5081
|
}
|
|
4953
5082
|
let derived: unknown
|
|
4954
5083
|
try {
|
|
@@ -4960,7 +5089,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4960
5089
|
return derived.then(
|
|
4961
5090
|
(settled) => {
|
|
4962
5091
|
if (isResponseResult(settled) || settled instanceof Response) {
|
|
4963
|
-
return
|
|
5092
|
+
return this.respondFused(settled, ctx)
|
|
4964
5093
|
}
|
|
4965
5094
|
Object.assign(ctx, settled as object)
|
|
4966
5095
|
return runBefore()
|
|
@@ -4969,7 +5098,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4969
5098
|
)
|
|
4970
5099
|
}
|
|
4971
5100
|
if (isResponseResult(derived) || derived instanceof Response) {
|
|
4972
|
-
return
|
|
5101
|
+
return this.respondFused(derived, ctx)
|
|
4973
5102
|
}
|
|
4974
5103
|
Object.assign(ctx, derived as object)
|
|
4975
5104
|
return runBefore()
|
|
@@ -5005,6 +5134,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5005
5134
|
(response) => this.wrapWebResponse(response),
|
|
5006
5135
|
(err) => Promise.resolve(logError(err, ctx)),
|
|
5007
5136
|
bodySchema,
|
|
5137
|
+
this.formOrigins,
|
|
5008
5138
|
) as MaybePromise<Response>
|
|
5009
5139
|
}
|
|
5010
5140
|
}
|
|
@@ -5436,6 +5566,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5436
5566
|
wrapResponse,
|
|
5437
5567
|
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
5438
5568
|
bodySchema,
|
|
5569
|
+
this.formOrigins,
|
|
5439
5570
|
)
|
|
5440
5571
|
}
|
|
5441
5572
|
}
|
|
@@ -5472,7 +5603,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5472
5603
|
entry: RouteEntry,
|
|
5473
5604
|
ctx: RawContext,
|
|
5474
5605
|
run: () => MaybePromise<T>,
|
|
5475
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5606
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5476
5607
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5477
5608
|
): MaybePromise<T> {
|
|
5478
5609
|
let outcome: MaybePromise<T>
|
|
@@ -5492,7 +5623,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5492
5623
|
entry: RouteEntry,
|
|
5493
5624
|
source: RequestSource,
|
|
5494
5625
|
ctx: RawContext,
|
|
5495
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5626
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5496
5627
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5497
5628
|
): MaybePromise<T> {
|
|
5498
5629
|
return executeRouteProgram(this, entry, entry.program, source, ctx, finalize, wrapResponse)
|
|
@@ -5536,7 +5667,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5536
5667
|
entry: RouteEntry,
|
|
5537
5668
|
error: unknown,
|
|
5538
5669
|
ctx: RawContext,
|
|
5539
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5670
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5540
5671
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5541
5672
|
): MaybePromise<T> {
|
|
5542
5673
|
return this.handleLifecycleError(entry, error, ctx, finalize, wrapResponse)
|
|
@@ -5548,7 +5679,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5548
5679
|
entry: RouteEntry,
|
|
5549
5680
|
ctx: RawContext,
|
|
5550
5681
|
result: unknown,
|
|
5551
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5682
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5552
5683
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5553
5684
|
): MaybePromise<T> {
|
|
5554
5685
|
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result)
|
|
@@ -5577,7 +5708,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5577
5708
|
entry: RouteEntry,
|
|
5578
5709
|
source: RequestSource,
|
|
5579
5710
|
ctx: RawContext,
|
|
5580
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5711
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5581
5712
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5582
5713
|
): Promise<T> {
|
|
5583
5714
|
return readBodyFramed(
|
|
@@ -5598,7 +5729,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5598
5729
|
entry: RouteEntry,
|
|
5599
5730
|
parsed: unknown,
|
|
5600
5731
|
ctx: RawContext,
|
|
5601
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5732
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5602
5733
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5603
5734
|
): MaybePromise<T> {
|
|
5604
5735
|
try {
|
|
@@ -5633,14 +5764,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5633
5764
|
private executeHandler<T>(
|
|
5634
5765
|
entry: RouteEntry,
|
|
5635
5766
|
ctx: RawContext,
|
|
5636
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5767
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5637
5768
|
wrapResponse?: (response: Response | ResponseResult) => T,
|
|
5638
5769
|
): MaybePromise<T> {
|
|
5639
5770
|
if (entry.hasDecorations) Object.assign(ctx, entry.decorations)
|
|
5640
5771
|
const handlerOutput = entry.handler(ctx)
|
|
5641
5772
|
if (handlerOutput instanceof Promise) {
|
|
5642
5773
|
return handlerOutput.then(
|
|
5643
|
-
(value) => finalize(value, responseSet(ctx)),
|
|
5774
|
+
(value) => finalize(value, responseSet(ctx), ctx),
|
|
5644
5775
|
(err) => {
|
|
5645
5776
|
if (wrapResponse) {
|
|
5646
5777
|
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
@@ -5649,14 +5780,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5649
5780
|
},
|
|
5650
5781
|
)
|
|
5651
5782
|
}
|
|
5652
|
-
return finalize(handlerOutput, responseSet(ctx))
|
|
5783
|
+
return finalize(handlerOutput, responseSet(ctx), ctx)
|
|
5653
5784
|
}
|
|
5654
5785
|
|
|
5655
5786
|
private handleValidationErrorRecovery<T>(
|
|
5656
5787
|
entry: RouteEntry,
|
|
5657
5788
|
recovery: unknown,
|
|
5658
5789
|
ctx: RawContext,
|
|
5659
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5790
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5660
5791
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5661
5792
|
originalIssues: ReadonlyArray<StandardIssue>,
|
|
5662
5793
|
kind: "body" | "query",
|
|
@@ -5706,7 +5837,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5706
5837
|
entry: RouteEntry,
|
|
5707
5838
|
result: StandardResult<unknown>,
|
|
5708
5839
|
ctx: RawContext,
|
|
5709
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5840
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5710
5841
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5711
5842
|
): MaybePromise<T> {
|
|
5712
5843
|
if (result.issues !== undefined) {
|
|
@@ -5746,7 +5877,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5746
5877
|
private runQueryOnly<T>(
|
|
5747
5878
|
entry: RouteEntry,
|
|
5748
5879
|
ctx: RawContext,
|
|
5749
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5880
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5750
5881
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5751
5882
|
): MaybePromise<T> {
|
|
5752
5883
|
try {
|
|
@@ -5784,7 +5915,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5784
5915
|
entry: RouteEntry,
|
|
5785
5916
|
result: StandardResult<unknown>,
|
|
5786
5917
|
ctx: RawContext,
|
|
5787
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5918
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5788
5919
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5789
5920
|
): MaybePromise<T> {
|
|
5790
5921
|
if (result.issues !== undefined) {
|
|
@@ -5829,13 +5960,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5829
5960
|
*/
|
|
5830
5961
|
private applyOnResponse(response: Response, req: Request): MaybePromise<Response> {
|
|
5831
5962
|
const hooks = this.onResponseHooks
|
|
5832
|
-
|
|
5963
|
+
// Hooks write onto the response they are handed (cookies, CORS, request ids), so every response
|
|
5964
|
+
// entering the chain - and any other one a hook hands back - is first made this request's own.
|
|
5965
|
+
let current = writableResponse(response, req)
|
|
5833
5966
|
for (let i = 0; i < hooks.length; i++) {
|
|
5834
5967
|
const next = (hooks[i] as RawOnResponse)(current, req)
|
|
5835
5968
|
if (next instanceof Promise) {
|
|
5836
|
-
|
|
5969
|
+
const handed = current
|
|
5970
|
+
return next.then((settled) => this.continueOnResponse(settled, i + 1, req, handed))
|
|
5837
5971
|
}
|
|
5838
|
-
current = next
|
|
5972
|
+
current = next === current ? next : writableResponse(next, req)
|
|
5839
5973
|
}
|
|
5840
5974
|
return current
|
|
5841
5975
|
}
|
|
@@ -5909,11 +6043,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5909
6043
|
response: Response,
|
|
5910
6044
|
nextIndex: number,
|
|
5911
6045
|
req: Request,
|
|
6046
|
+
handed: Response,
|
|
5912
6047
|
): Promise<Response> {
|
|
5913
|
-
let current = response
|
|
6048
|
+
let current = response === handed ? response : writableResponse(response, req)
|
|
5914
6049
|
for (let i = nextIndex; i < this.onResponseHooks.length; i++) {
|
|
5915
6050
|
const next = (this.onResponseHooks[i] as RawOnResponse)(current, req)
|
|
5916
|
-
|
|
6051
|
+
const settled = next instanceof Promise ? await next : next
|
|
6052
|
+
current = settled === current ? settled : writableResponse(settled, req)
|
|
5917
6053
|
}
|
|
5918
6054
|
return current
|
|
5919
6055
|
}
|
|
@@ -5945,13 +6081,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5945
6081
|
private finishLifecycleContract<T>(
|
|
5946
6082
|
entry: RouteEntry,
|
|
5947
6083
|
ctx: RawContext,
|
|
5948
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
6084
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5949
6085
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5950
6086
|
result: unknown,
|
|
5951
6087
|
): MaybePromise<T> {
|
|
5952
6088
|
try {
|
|
5953
6089
|
const contract = entry.responseContract
|
|
5954
|
-
if (contract === undefined) return finalize(result, responseSet(ctx))
|
|
6090
|
+
if (contract === undefined) return finalize(result, responseSet(ctx), ctx)
|
|
5955
6091
|
const checked = contract.runtime.check(contract.definition, result, ctx)
|
|
5956
6092
|
if (checked instanceof Promise) {
|
|
5957
6093
|
return checked.then(
|
|
@@ -5967,7 +6103,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5967
6103
|
|
|
5968
6104
|
private finishContractOutcome<T>(
|
|
5969
6105
|
ctx: RawContext,
|
|
5970
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
6106
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5971
6107
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5972
6108
|
outcome: {
|
|
5973
6109
|
readonly kind: "ok" | "warn" | "violation"
|
|
@@ -5990,7 +6126,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5990
6126
|
detail: outcome.message,
|
|
5991
6127
|
})
|
|
5992
6128
|
}
|
|
5993
|
-
return finalize(outcome.value, responseSet(ctx))
|
|
6129
|
+
return finalize(outcome.value, responseSet(ctx), ctx)
|
|
5994
6130
|
}
|
|
5995
6131
|
|
|
5996
6132
|
/** Synchronous by design, and `MaybePromise` rather than `Promise` for the same reason: control flow
|
|
@@ -6002,7 +6138,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6002
6138
|
entry: RouteEntry,
|
|
6003
6139
|
err: unknown,
|
|
6004
6140
|
ctx: RawContext,
|
|
6005
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
6141
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
6006
6142
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6007
6143
|
): MaybePromise<T> {
|
|
6008
6144
|
// A *thrown* Response is deliberate control flow, not an error - a guard throws a redirect/401,
|
|
@@ -6010,10 +6146,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6010
6146
|
// SvelteKit semantics): as built, plus the cookies the request queued, so a session set or
|
|
6011
6147
|
// cleared before `throw redirect(...)` / `requireSession(...)` ships. Don't run onError or log
|
|
6012
6148
|
// it as a 500.
|
|
6013
|
-
if (err instanceof Response
|
|
6014
|
-
|
|
6015
|
-
//
|
|
6016
|
-
if (isResponseResult(err))
|
|
6149
|
+
if (err instanceof Response && err !== ctx.jsonReply)
|
|
6150
|
+
return finalize(err, responseSet(ctx), ctx)
|
|
6151
|
+
// Same rule for a thrown `status(...)` or `c.json(...)`, which stay held to the route's contract.
|
|
6152
|
+
if (err instanceof Response || isResponseResult(err))
|
|
6017
6153
|
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, err)
|
|
6018
6154
|
if (entry.onError.length === 0) {
|
|
6019
6155
|
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
@@ -6030,7 +6166,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6030
6166
|
entry: RouteEntry,
|
|
6031
6167
|
err: unknown,
|
|
6032
6168
|
ctx: RawContext,
|
|
6033
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
6169
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
6034
6170
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6035
6171
|
): Promise<T> {
|
|
6036
6172
|
// Indexed for the same reason as the `beforeHandle` chain above: the iterator would outlive
|
|
@@ -6038,7 +6174,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6038
6174
|
for (let i = 0; i < entry.onError.length; i++) {
|
|
6039
6175
|
const outcome = entry.onError[i]!(err, ctx)
|
|
6040
6176
|
const handled = outcome instanceof Promise ? await outcome : outcome
|
|
6041
|
-
if (handled
|
|
6177
|
+
if (handled === undefined) continue
|
|
6178
|
+
const contract = entry.responseContract
|
|
6179
|
+
if (contract === undefined || (handled !== ctx.jsonReply && !isResponseResult(handled)))
|
|
6180
|
+
return finalize(handled, responseSet(ctx), ctx)
|
|
6181
|
+
// Checked here, not through finishLifecycleContract: a throwing schema must not re-enter onError.
|
|
6182
|
+
try {
|
|
6183
|
+
const checked = await contract.runtime.check(contract.definition, handled, ctx)
|
|
6184
|
+
return this.finishContractOutcome(ctx, finalize, wrapResponse, checked)
|
|
6185
|
+
} catch (checkError) {
|
|
6186
|
+
this.logRequestError(checkError, ctx)
|
|
6187
|
+
return wrapResponse(plainError(500, "internal_error"))
|
|
6188
|
+
}
|
|
6042
6189
|
}
|
|
6043
6190
|
this.logRequestError(err, ctx)
|
|
6044
6191
|
return wrapResponse(plainError(500, "internal_error"))
|
|
@@ -6075,6 +6222,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6075
6222
|
(response) => response,
|
|
6076
6223
|
(error) => Promise.reject(error),
|
|
6077
6224
|
bodySchema,
|
|
6225
|
+
this.formOrigins,
|
|
6078
6226
|
)
|
|
6079
6227
|
}
|
|
6080
6228
|
|
|
@@ -6166,7 +6314,22 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6166
6314
|
request: Request,
|
|
6167
6315
|
params: Record<string, string>,
|
|
6168
6316
|
): MaybePromise<Response> => {
|
|
6169
|
-
|
|
6317
|
+
let outcome: MaybePromise<Response>
|
|
6318
|
+
try {
|
|
6319
|
+
outcome = capped!(request, params, undefined, signal!, budget!, peer, true)
|
|
6320
|
+
} catch (err) {
|
|
6321
|
+
// A synchronous render throw, as in runMatchedLanes.
|
|
6322
|
+
outcome = this.contextlessBareError(
|
|
6323
|
+
err,
|
|
6324
|
+
request,
|
|
6325
|
+
params,
|
|
6326
|
+
undefined,
|
|
6327
|
+
signal!,
|
|
6328
|
+
budget!,
|
|
6329
|
+
peer,
|
|
6330
|
+
this.wrapWebResponse,
|
|
6331
|
+
)
|
|
6332
|
+
}
|
|
6170
6333
|
return outcome instanceof Promise
|
|
6171
6334
|
? outcome.then((response) => finishNative(request, response))
|
|
6172
6335
|
: finishNative(request, outcome)
|
|
@@ -6183,7 +6346,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6183
6346
|
markFramed(request)
|
|
6184
6347
|
return request.headers.get(NIFRA_DEADLINE_HEADER) !== null
|
|
6185
6348
|
? this.fetchMatched(request, entry, EMPTY_PARAMS, peer)
|
|
6186
|
-
:
|
|
6349
|
+
: runNative(request, EMPTY_PARAMS)
|
|
6187
6350
|
}
|
|
6188
6351
|
}
|
|
6189
6352
|
return (request) => {
|
|
@@ -6211,7 +6374,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6211
6374
|
if (malformed(params)) return fallback(request, server)
|
|
6212
6375
|
return request.headers.get(NIFRA_DEADLINE_HEADER) !== null
|
|
6213
6376
|
? this.fetchMatched(request, entry, params, peer)
|
|
6214
|
-
:
|
|
6377
|
+
: runNative(request, params)
|
|
6215
6378
|
}
|
|
6216
6379
|
}
|
|
6217
6380
|
return (request, server) => {
|
|
@@ -6239,7 +6402,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6239
6402
|
// resolves) instead of Bun's native table. The allocation-free default keeps native fusion.
|
|
6240
6403
|
if (
|
|
6241
6404
|
(this.onRequestHooks.length > 0 && !this.bunNativeRequestHooksSafe) ||
|
|
6242
|
-
this.
|
|
6405
|
+
this.wsRoutes.length > 0 ||
|
|
6243
6406
|
this.preRouteMountCount > 0 ||
|
|
6244
6407
|
this.clientIpTrust !== undefined
|
|
6245
6408
|
) {
|
|
@@ -6396,7 +6559,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6396
6559
|
}
|
|
6397
6560
|
// Seal before Bun.serve: in `"error"` mode this throws instead of leaving a bound port behind.
|
|
6398
6561
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
6399
|
-
sealHookAudit(this, this.catalog.size, this.logger)
|
|
6562
|
+
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger)
|
|
6400
6563
|
// Bun's `Server` is the concrete handle; we expose the stable `RunningServer`
|
|
6401
6564
|
// subset so the public types don't depend on the ambient `Bun` global. The cast
|
|
6402
6565
|
// bridges them - Bun's `.port` is `number | undefined` (undefined only for unix
|
|
@@ -6407,7 +6570,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6407
6570
|
// `websocket` handlers are one shared dispatcher - each connection's `ws.data.handler` is the
|
|
6408
6571
|
// matched route's handler, set by `server.upgrade`.
|
|
6409
6572
|
// With WS routes, the dispatcher comes from the installed `.use(websocket())` runtime - non-null
|
|
6410
|
-
// because
|
|
6573
|
+
// because a socket route means ws() ran, and ws() requires the runtime at registration.
|
|
6411
6574
|
// Native pub/sub when the app has WS routes and none validate outbound frames: `ws.subscribe` and
|
|
6412
6575
|
// `app.publish` go through Bun's own (uWebSockets) broadcast instead of the JS registry loop.
|
|
6413
6576
|
// A mounted app counts only while it has a WebSocket route (see `[GET_WS_RUNTIME]`), so composing
|
|
@@ -6424,8 +6587,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6424
6587
|
// that registry in the upgrade outcome instead of accidentally broadcasting child sockets through
|
|
6425
6588
|
// the parent.
|
|
6426
6589
|
const nativePubsub =
|
|
6427
|
-
this.
|
|
6428
|
-
const wsHandlers = wsRuntime?.bunHandlers(
|
|
6590
|
+
this.wsRoutes.length > 0 && !this.mountsTakeUpgrades() && !this.wsHasValidatedSend
|
|
6591
|
+
const wsHandlers = wsRuntime?.bunHandlers(
|
|
6592
|
+
this.topics ?? wsRuntime.createTopics(),
|
|
6593
|
+
nativePubsub,
|
|
6594
|
+
this,
|
|
6595
|
+
)
|
|
6429
6596
|
// Spread rather than pass `hostname: undefined` (or an undefined idle timeout or TLS) - Bun treats
|
|
6430
6597
|
// an explicit undefined as a value on some option paths, and omitting is what selects its own
|
|
6431
6598
|
// defaults, 0.0.0.0 among them.
|
|
@@ -6473,8 +6640,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6473
6640
|
// `unknown` params bridge a TS structural-variance quirk. Round-trip covered by websocket.test.ts.
|
|
6474
6641
|
websocket: {
|
|
6475
6642
|
// Cap inbound frames so a huge message can't be buffered/JSON-parsed into memory; the runtime
|
|
6476
|
-
// closes an over-cap connection before the handler runs. Default 1 MB (maxBodyBytes).
|
|
6477
|
-
|
|
6643
|
+
// closes an over-cap connection before the handler runs. Default 1 MB (maxBodyBytes). One
|
|
6644
|
+
// limit per listener: the largest cap among this app and the apps mounted on it.
|
|
6645
|
+
maxPayloadLength: wsHandlers.maxPayloadLength,
|
|
6478
6646
|
open: (ws) => wsHandlers.open(ws),
|
|
6479
6647
|
message: (ws, message) => wsHandlers.message(ws, message),
|
|
6480
6648
|
close: (ws, code, reason) => wsHandlers.close(ws, code, reason),
|
|
@@ -6492,7 +6660,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6492
6660
|
}
|
|
6493
6661
|
}
|
|
6494
6662
|
this.sealed = true
|
|
6495
|
-
if (this.gracefulSignals) this.installSignalHandlers()
|
|
6663
|
+
if (this.gracefulSignals) this.installSignalHandlers(this.gracefulSignals)
|
|
6496
6664
|
return running
|
|
6497
6665
|
}
|
|
6498
6666
|
|
|
@@ -6525,12 +6693,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6525
6693
|
])
|
|
6526
6694
|
}
|
|
6527
6695
|
|
|
6528
|
-
private installSignalHandlers(): void {
|
|
6696
|
+
private installSignalHandlers(drain: { readonly drainMs?: number }): void {
|
|
6529
6697
|
// Drain, then let the process exit naturally - the stopped server no longer
|
|
6530
6698
|
// holds the event loop open. Opt-in (`gracefulSignals`), so taking over the
|
|
6531
6699
|
// signals is consented; we don't force `process.exit`.
|
|
6532
6700
|
const onSignal = (): void => {
|
|
6533
|
-
void this.stop()
|
|
6701
|
+
void this.stop(drain)
|
|
6534
6702
|
}
|
|
6535
6703
|
process.once("SIGTERM", onSignal)
|
|
6536
6704
|
process.once("SIGINT", onSignal)
|