@nifrajs/core 4.0.0 → 5.0.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 +71 -5
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +90 -25
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +6 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +52 -0
- 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 +7 -2
- 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 +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- 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-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/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.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.map +1 -1
- package/dist/server/context.d.ts +2 -1
- package/dist/server/context.d.ts.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-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 +29 -5
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +44 -31
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +444 -321
- 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 +22 -14
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +21 -0
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-hook.d.ts +7 -1
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- 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 +20 -1
- 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/ws.d.ts.map +1 -1
- package/dist/ws.js +3 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/assurance.ts +180 -31
- package/src/capabilities.ts +59 -0
- package/src/durable-effect.ts +1 -1
- package/src/durable-saga.ts +94 -10
- package/src/edge-kit.ts +7 -2
- package/src/effect-scope.ts +15 -5
- package/src/errors.ts +1 -0
- package/src/evidence.ts +12 -3
- package/src/index.ts +6 -1
- 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-program.ts +98 -44
- 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.ts +34 -1
- package/src/server/client-ip.ts +3 -0
- package/src/server/context.ts +2 -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-observer-runtime.ts +16 -38
- package/src/server/runtime-core.ts +2 -0
- package/src/server/server-types.ts +29 -5
- package/src/server/server.ts +659 -530
- 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 +44 -5
- package/src/server/ws-hook.ts +5 -0
- package/src/server.ts +5 -0
- package/src/single-copy.ts +20 -1
- package/src/tool-contract.ts +67 -39
- package/src/ws.ts +4 -0
package/src/server/server.ts
CHANGED
|
@@ -17,6 +17,15 @@ 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,
|
|
@@ -107,6 +116,7 @@ import {
|
|
|
107
116
|
fusedRespond,
|
|
108
117
|
fusedRespondNoSet,
|
|
109
118
|
toResponse,
|
|
119
|
+
writableResponse,
|
|
110
120
|
} from "./respond.ts"
|
|
111
121
|
// Type-only: erased, so the kernel never pulls the lane's implementation into a bundle that does not
|
|
112
122
|
// install the plugin. The value side arrives through the symbol-keyed install seam.
|
|
@@ -116,6 +126,7 @@ import type {
|
|
|
116
126
|
ResponseObserverRuntime,
|
|
117
127
|
} from "./response-observer-runtime.ts"
|
|
118
128
|
import {
|
|
129
|
+
CONTEXT_REQUEST,
|
|
119
130
|
CONTEXT_SEARCH,
|
|
120
131
|
CONTEXT_SET,
|
|
121
132
|
EMPTY_RESPONSE_CONTROLS,
|
|
@@ -160,6 +171,7 @@ import {
|
|
|
160
171
|
INSTALL_SSE,
|
|
161
172
|
INSTALL_WS,
|
|
162
173
|
NODE_NATIVE_MOUNT,
|
|
174
|
+
REPORT_ERROR,
|
|
163
175
|
RESOLVE_NODE_MOUNT,
|
|
164
176
|
} from "./install.ts"
|
|
165
177
|
import type { EffectLedgerRuntime } from "./ledger-lane.ts"
|
|
@@ -186,6 +198,7 @@ import type {
|
|
|
186
198
|
import type {
|
|
187
199
|
AdmissionController,
|
|
188
200
|
AdmissionDecision,
|
|
201
|
+
DeclaredMountRoute,
|
|
189
202
|
FetchHandler,
|
|
190
203
|
ListenTlsOptions,
|
|
191
204
|
McpPromptDescriptor,
|
|
@@ -194,6 +207,7 @@ import type {
|
|
|
194
207
|
MountableApp,
|
|
195
208
|
MountFetchOptions,
|
|
196
209
|
MountOptions,
|
|
210
|
+
OpaqueMountDeclaration,
|
|
197
211
|
PromptArgument,
|
|
198
212
|
PromptMessage,
|
|
199
213
|
ResponseFinalization,
|
|
@@ -205,12 +219,7 @@ import type {
|
|
|
205
219
|
} from "./server-types.ts"
|
|
206
220
|
import type { SSEInit, TypedSSEStream } from "./sse.ts"
|
|
207
221
|
import type { SseRuntime } from "./sse-hook.ts"
|
|
208
|
-
import type {
|
|
209
|
-
TopicRegistry,
|
|
210
|
-
WebSocketContext,
|
|
211
|
-
WebSocketHandler,
|
|
212
|
-
WebSocketUpgradeOutcome,
|
|
213
|
-
} from "./websocket.ts"
|
|
222
|
+
import type { TopicRegistry, WebSocketHandler, WebSocketUpgradeOutcome } from "./websocket.ts"
|
|
214
223
|
import type { BunWsData } from "./ws-bun.ts"
|
|
215
224
|
import type { WsRuntime } from "./ws-hook.ts"
|
|
216
225
|
|
|
@@ -285,6 +294,7 @@ export interface RawContext {
|
|
|
285
294
|
readonly jsonReply?: Response | undefined
|
|
286
295
|
readonly jsonBody?: unknown
|
|
287
296
|
readonly [CONTEXT_SEARCH]: string
|
|
297
|
+
readonly [CONTEXT_REQUEST]: Request
|
|
288
298
|
readonly signal: AbortSignal
|
|
289
299
|
readonly budget: RequestBudget
|
|
290
300
|
readonly env: unknown
|
|
@@ -297,14 +307,18 @@ export interface RawContext {
|
|
|
297
307
|
/** Broad shape so the implementation signature is compatible with both typed overloads. */
|
|
298
308
|
type ErasedHandler = (ctx: never) => MaybePromise<HandlerResult>
|
|
299
309
|
|
|
300
|
-
|
|
310
|
+
/** What an `onRequest` hook returns: a reply (a `Response`, or a `status()`/`json()` result that takes
|
|
311
|
+
* the normal response path), a replacement `Request`, or `undefined` to continue. */
|
|
312
|
+
export type OnRequestResult = Response | ResponseResult | Request | undefined
|
|
301
313
|
type RawOnRequest = (req: Request, platform?: Platform) => MaybePromise<OnRequestResult>
|
|
302
314
|
type RawOnResponse = (response: Response, req: Request) => MaybePromise<Response>
|
|
303
315
|
type RawOnResponseFinalized = (outcome: ResponseFinalization, req: Request) => MaybePromise<void>
|
|
304
316
|
|
|
305
|
-
/** A registered WebSocket route
|
|
317
|
+
/** A registered WebSocket route and the route chain its handshake runs; matching reuses
|
|
318
|
+
* {@link Router} under the GET verb. */
|
|
306
319
|
interface WsEntry {
|
|
307
320
|
readonly handler: WebSocketHandler
|
|
321
|
+
readonly chain: RouteEntry
|
|
308
322
|
}
|
|
309
323
|
|
|
310
324
|
/** Structural view of the socket-peer lookup on the Bun `Server`, so any Bun `Server` (WS or not)
|
|
@@ -353,10 +367,14 @@ interface FetchMount<Env = unknown> {
|
|
|
353
367
|
readonly fallbackOn404: boolean
|
|
354
368
|
readonly beforeRoutes: boolean
|
|
355
369
|
readonly order: number
|
|
356
|
-
/**
|
|
357
|
-
readonly opaque: string | undefined
|
|
370
|
+
/** Why this child is not analyzed (`MountOptions.opaque`), checked by reflection. */
|
|
371
|
+
readonly opaque: string | OpaqueMountDeclaration | undefined
|
|
358
372
|
/** The mounted app (`mount()` only), for reflection to tell a composed child from an opaque one. */
|
|
359
373
|
readonly app?: object
|
|
374
|
+
/** Parent request → the stripped copy a handshake check handed this child. On a pass the HTTP
|
|
375
|
+
* dispatch hands over that same copy, which the child's own pass record knows, so its onRequest
|
|
376
|
+
* hooks do not run a second time. */
|
|
377
|
+
wsPassed?: WeakMap<Request, Request>
|
|
360
378
|
}
|
|
361
379
|
|
|
362
380
|
/** The socket peer Bun observed, as a `Platform` for the request lifecycle (`undefined` if unknown). */
|
|
@@ -410,6 +428,7 @@ function outranks(
|
|
|
410
428
|
}
|
|
411
429
|
|
|
412
430
|
const WS_PASS: WebSocketUpgradeOutcome = { kind: "pass" }
|
|
431
|
+
|
|
413
432
|
const DEFAULT_WS_UPGRADE_TIMEOUT_MS = 10_000
|
|
414
433
|
/** Counts `ws()` routes and `mount()`s across every app in this copy of core: the only
|
|
415
434
|
* changes that can make a mounted app take WebSocket upgrades. An app's answer to that question
|
|
@@ -427,6 +446,9 @@ function requireWsRuntime(runtime: WsRuntime | undefined): WsRuntime {
|
|
|
427
446
|
return runtime
|
|
428
447
|
}
|
|
429
448
|
|
|
449
|
+
/** The method `app.sse()` registers: POST when the schema declares a body, which a GET cannot carry. */
|
|
450
|
+
type SseMethodOf<S> = S extends { readonly body: StandardSchemaV1 } ? "POST" : "GET"
|
|
451
|
+
|
|
430
452
|
function requireSseRuntime(runtime: SseRuntime | undefined): SseRuntime {
|
|
431
453
|
if (runtime === undefined) {
|
|
432
454
|
throw new FrameworkError(
|
|
@@ -495,6 +517,7 @@ export type Handler<
|
|
|
495
517
|
export type {
|
|
496
518
|
AdmissionController,
|
|
497
519
|
AdmissionDecision,
|
|
520
|
+
DeclaredMountRoute,
|
|
498
521
|
FetchHandler,
|
|
499
522
|
ListenTlsOptions,
|
|
500
523
|
McpPromptDescriptor,
|
|
@@ -503,6 +526,7 @@ export type {
|
|
|
503
526
|
MountableApp,
|
|
504
527
|
MountFetchOptions,
|
|
505
528
|
MountOptions,
|
|
529
|
+
OpaqueMountDeclaration,
|
|
506
530
|
PromptArgument,
|
|
507
531
|
PromptMessage,
|
|
508
532
|
ResponseFinalization,
|
|
@@ -675,11 +699,16 @@ const underPrefix = (prefix: string): HookScope => {
|
|
|
675
699
|
}
|
|
676
700
|
|
|
677
701
|
/** 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.
|
|
702
|
+
* the guard never reflects whatever the source's own catalog grows into afterwards. An `OPTIONS`
|
|
703
|
+
* request to a path they serve is in scope too: a CORS preflight names the path, not a route, and
|
|
704
|
+
* the group's `cors()` is the only hook that can answer it. */
|
|
679
705
|
const servedBy = (routes: readonly CatalogRoute[]): HookScope => {
|
|
680
706
|
const scope = new RouteCatalog()
|
|
681
707
|
scope.addBatch(routes)
|
|
682
|
-
return (req) =>
|
|
708
|
+
return (req) => {
|
|
709
|
+
const match = scope.find(req.method, pathnameOf(req.url))
|
|
710
|
+
return match.found || (req.method === "OPTIONS" && match.reason === "method-not-allowed")
|
|
711
|
+
}
|
|
683
712
|
}
|
|
684
713
|
|
|
685
714
|
type AdoptedHook = ((first: never, second: never) => unknown) | undefined
|
|
@@ -800,6 +829,30 @@ interface HookAuditState {
|
|
|
800
829
|
}
|
|
801
830
|
const hookAudit = /* @__PURE__ */ new WeakMap<object, HookAuditState>()
|
|
802
831
|
|
|
832
|
+
/** Whether `from` reaches `target` through mounts that keep the path and take a request under `path`.
|
|
833
|
+
* Module-level, not a `Server` method, so a production bundle drops it with its dev-only caller. */
|
|
834
|
+
function mountLeadsBack(
|
|
835
|
+
from: unknown,
|
|
836
|
+
target: unknown,
|
|
837
|
+
path: string,
|
|
838
|
+
mountsOf: (server: Server) => readonly FetchMount[],
|
|
839
|
+
seen: Set<unknown>,
|
|
840
|
+
): boolean {
|
|
841
|
+
if (from === target) return true
|
|
842
|
+
if (!(from instanceof Server) || seen.has(from)) return false
|
|
843
|
+
seen.add(from)
|
|
844
|
+
const under = (prefix: string): string => (prefix === "/" ? "/" : `${prefix}/`)
|
|
845
|
+
return mountsOf(from).some(
|
|
846
|
+
(mount) =>
|
|
847
|
+
!mount.stripPrefix &&
|
|
848
|
+
(under(path).startsWith(under(mount.path)) || under(mount.path).startsWith(under(path))) &&
|
|
849
|
+
mountLeadsBack(mount.app, target, path, mountsOf, seen),
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** Set by `@nifrajs/web`'s `createWebApp` on its app once the page routes are declared. */
|
|
854
|
+
const PAGES_DECLARED = /* @__PURE__ */ Symbol.for("nifra.web.pagesDeclared")
|
|
855
|
+
|
|
803
856
|
/** Start tracking one server's order-scoped hook pushes (once, from the constructor). */
|
|
804
857
|
const beginHookAudit = (
|
|
805
858
|
key: object,
|
|
@@ -810,13 +863,42 @@ const beginHookAudit = (
|
|
|
810
863
|
}
|
|
811
864
|
|
|
812
865
|
/** 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.
|
|
866
|
+
* seal means no route was declared after it) and the caller's frame. Hooks a WebSocket handshake runs
|
|
867
|
+
* count sockets as routes; `around` and `afterHandle` wrap a response, so they count HTTP routes only. */
|
|
814
868
|
const recordScopedHook = (key: object, routesAtPush: number, kind: string): void => {
|
|
869
|
+
// Not a dead hook when routes follow it, but every page already copied its chain without it.
|
|
870
|
+
if (PAGES_DECLARED in key) {
|
|
871
|
+
const site = captureCallerSite()
|
|
872
|
+
throw new FrameworkError(
|
|
873
|
+
"HOOK_AFTER_PAGES",
|
|
874
|
+
`${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.`,
|
|
875
|
+
)
|
|
876
|
+
}
|
|
815
877
|
const state = hookAudit.get(key)
|
|
816
878
|
if (state === undefined || state.policy === "off") return
|
|
817
879
|
state.sites.push({ kind, routesAtPush, site: captureCallerSite() })
|
|
818
880
|
}
|
|
819
881
|
|
|
882
|
+
/** Hook provenance for an adoption (dev only, see `internal/hook-origins.ts`): which scope each
|
|
883
|
+
* wrapper just appended to a hook list gates its hook by. `pairs` holds each source list with the target list it
|
|
884
|
+
* was appended to, read right after the append so the wrappers are the target list's tail. */
|
|
885
|
+
const tagAdopted = (
|
|
886
|
+
target: object,
|
|
887
|
+
source: object,
|
|
888
|
+
prefix: string | undefined,
|
|
889
|
+
routes: readonly CatalogRoute[],
|
|
890
|
+
pairs: readonly (readonly [readonly unknown[], readonly unknown[]])[],
|
|
891
|
+
): void => {
|
|
892
|
+
tagAdoption(target, source, prefix)
|
|
893
|
+
const scope =
|
|
894
|
+
prefix !== undefined
|
|
895
|
+
? { prefix }
|
|
896
|
+
: { routes: routes.map((route) => ({ method: route.method, path: route.path })) }
|
|
897
|
+
for (const [originals, appended] of pairs) {
|
|
898
|
+
if (originals.length > 0) tagScoped(originals, appended.slice(-originals.length), scope)
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
820
902
|
/** Run the deadness check exactly once. A hook whose recorded route count equals the final route count
|
|
821
903
|
* had no route declared after it - unambiguously dead, since the scoping feature (a hook that covers
|
|
822
904
|
* only later routes) always leaves at least one such route. Zero-route apps are exempt (different
|
|
@@ -824,13 +906,20 @@ const recordScopedHook = (key: object, routesAtPush: number, kind: string): void
|
|
|
824
906
|
const sealHookAudit = (
|
|
825
907
|
key: object,
|
|
826
908
|
total: number,
|
|
909
|
+
sockets: number,
|
|
827
910
|
logger: { warn(message: string): void },
|
|
828
911
|
): void => {
|
|
829
912
|
const state = hookAudit.get(key)
|
|
830
913
|
if (state === undefined || state.checked) return
|
|
831
914
|
state.checked = true
|
|
832
|
-
if (state.policy === "off" || total === 0) return
|
|
833
|
-
const dead = state.sites.filter(
|
|
915
|
+
if (state.policy === "off" || total + sockets === 0) return
|
|
916
|
+
const dead = state.sites.filter(
|
|
917
|
+
(hook) =>
|
|
918
|
+
hook.routesAtPush ===
|
|
919
|
+
(hook.kind === "around" || hook.kind === "aroundCapability" || hook.kind === "afterHandle"
|
|
920
|
+
? total
|
|
921
|
+
: total + sockets),
|
|
922
|
+
)
|
|
834
923
|
if (dead.length === 0) return
|
|
835
924
|
const detail = dead.map((hook) => ` ${hook.kind}() at ${hook.site ?? "<unknown>"}`).join("\n")
|
|
836
925
|
const message =
|
|
@@ -865,7 +954,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
865
954
|
private readonly catalog: RouteCatalog
|
|
866
955
|
/** WebSocket routes, matched separately at upgrade time (a GET + `Upgrade: websocket`). */
|
|
867
956
|
private readonly wsRouter: Router<WsEntry>
|
|
868
|
-
|
|
957
|
+
/** Each socket path, its declared capabilities, the evidence declarations its handshake captured
|
|
958
|
+
* and the chain it runs; reflection reads it. */
|
|
959
|
+
private readonly wsRoutes: {
|
|
960
|
+
readonly path: string
|
|
961
|
+
readonly capabilities: readonly string[] | undefined
|
|
962
|
+
readonly assurance: readonly AssuranceDeclaration[]
|
|
963
|
+
readonly chain: RouteEntry
|
|
964
|
+
}[]
|
|
869
965
|
/** `wsChanges` when `wsMountsLive` was last worked out; -1 until then. */
|
|
870
966
|
private wsMountsCheckedAt = -1
|
|
871
967
|
/** Whether an app mounted here takes WebSocket upgrades, as of `wsMountsCheckedAt`. */
|
|
@@ -899,7 +995,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
899
995
|
private readonly acceptInboundDeadlines: boolean
|
|
900
996
|
private readonly maxInboundDeadlineMs: number
|
|
901
997
|
private readonly deadlineAdmissionOptions: DeadlineAdmissionOptions
|
|
902
|
-
private readonly gracefulSignals:
|
|
998
|
+
private readonly gracefulSignals: { readonly drainMs?: number } | undefined
|
|
903
999
|
private readonly stopHooks: StopHook[]
|
|
904
1000
|
private readonly fetchMounts: FetchMount<EnvOf<Ctx>>[]
|
|
905
1001
|
private mountOrder: number
|
|
@@ -980,9 +1076,19 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
980
1076
|
/** Body/raw response hooks need a framework-payload marker; keep that decision per app. */
|
|
981
1077
|
private responseBodyTag: object | undefined
|
|
982
1078
|
private readonly responseBodyOwners: Set<object>
|
|
983
|
-
private readonly finalizeResponse = (result: unknown, set: CtxSet): Response =>
|
|
984
|
-
toResponse(
|
|
1079
|
+
private readonly finalizeResponse = (result: unknown, set: CtxSet, ctx?: RawContext): Response =>
|
|
1080
|
+
toResponse(
|
|
1081
|
+
result as HandlerResult,
|
|
1082
|
+
set,
|
|
1083
|
+
this.responseBodyTag,
|
|
1084
|
+
this.staticResponseHeaders,
|
|
1085
|
+
// Response hooks write next, and they run for this same request object.
|
|
1086
|
+
ctx !== undefined && this.onResponseHooks.length > 0 ? ctx[CONTEXT_REQUEST] : undefined,
|
|
1087
|
+
)
|
|
985
1088
|
private readonly responseRequests: WeakMap<Request, Request>
|
|
1089
|
+
/** A request the WebSocket handshake check ran the onRequest hooks on and then passed to HTTP →
|
|
1090
|
+
* what the hooks returned, so `fetch` routes that instead of running the hooks a second time. */
|
|
1091
|
+
private wsHookedPasses: WeakMap<object, Request> | undefined
|
|
986
1092
|
/** Original source → request observed by generic request hooks, including in-place mutations. */
|
|
987
1093
|
private readonly responseSources: WeakMap<object, Request>
|
|
988
1094
|
/** Memoized NodeRequestContext per plain-`Request` source - see {@link nodeRequestContextOf}. */
|
|
@@ -1006,7 +1112,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1006
1112
|
this.routePrefix = ""
|
|
1007
1113
|
this.catalog = new RouteCatalog()
|
|
1008
1114
|
this.wsRouter = new Router<WsEntry>()
|
|
1009
|
-
this.
|
|
1115
|
+
this.wsRoutes = []
|
|
1010
1116
|
this.topics = undefined
|
|
1011
1117
|
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
1012
1118
|
assertByteLimit(maxBodyBytes, "maxBodyBytes")
|
|
@@ -1018,7 +1124,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1018
1124
|
this.wsMaxPayloadBytes = wsMaxPayloadBytes
|
|
1019
1125
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 0
|
|
1020
1126
|
this.wsUpgradeTimeoutMs = options.wsUpgradeTimeoutMs ?? DEFAULT_WS_UPGRADE_TIMEOUT_MS
|
|
1021
|
-
|
|
1127
|
+
const trust = options.clientIp
|
|
1128
|
+
if (
|
|
1129
|
+
trust !== undefined &&
|
|
1130
|
+
!("header" in trust) &&
|
|
1131
|
+
!(Number.isSafeInteger(trust.trustedHops) && trust.trustedHops >= 0)
|
|
1132
|
+
) {
|
|
1133
|
+
throw new RangeError("clientIp trustedHops must be a non-negative integer")
|
|
1134
|
+
}
|
|
1135
|
+
this.clientIpTrust = trust
|
|
1022
1136
|
this.acceptInboundDeadlines = options.acceptInboundDeadlines ?? false
|
|
1023
1137
|
this.maxInboundDeadlineMs = options.maxInboundDeadlineMs ?? 30_000
|
|
1024
1138
|
this.deadlineAdmissionOptions = Object.freeze({
|
|
@@ -1034,7 +1148,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1034
1148
|
if (!Number.isFinite(this.maxInboundDeadlineMs) || this.maxInboundDeadlineMs <= 0) {
|
|
1035
1149
|
throw new RangeError("maxInboundDeadlineMs must be a finite positive number")
|
|
1036
1150
|
}
|
|
1037
|
-
this.gracefulSignals =
|
|
1151
|
+
this.gracefulSignals =
|
|
1152
|
+
options.gracefulSignals === true ? {} : options.gracefulSignals || undefined
|
|
1038
1153
|
this.stopHooks = []
|
|
1039
1154
|
this.fetchMounts = []
|
|
1040
1155
|
this.mountOrder = 0
|
|
@@ -1083,6 +1198,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1083
1198
|
this.responseBodyTag = undefined
|
|
1084
1199
|
this.responseBodyOwners = new Set()
|
|
1085
1200
|
this.responseRequests = new WeakMap()
|
|
1201
|
+
this.wsHookedPasses = undefined
|
|
1086
1202
|
this.responseSources = new WeakMap()
|
|
1087
1203
|
this.nodeContexts = new WeakMap()
|
|
1088
1204
|
this.appliedPlugins = new Map()
|
|
@@ -1187,6 +1303,20 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1187
1303
|
opaque: options.opaque,
|
|
1188
1304
|
app,
|
|
1189
1305
|
}
|
|
1306
|
+
// Mounts run before routes: a request under this prefix that no mount on the way strips comes back
|
|
1307
|
+
// here unchanged until the stack overflows, which takes the process down. Dev-time only, like the
|
|
1308
|
+
// hook audit: a production define folds this away, and the first dev request would hit it anyway.
|
|
1309
|
+
if (
|
|
1310
|
+
hookAuditRuntime &&
|
|
1311
|
+
process.env.NODE_ENV !== "production" &&
|
|
1312
|
+
!mount.stripPrefix &&
|
|
1313
|
+
mountLeadsBack(app, this, mount.path, (server) => server.fetchMounts, new Set())
|
|
1314
|
+
) {
|
|
1315
|
+
throw new RouteConfigError(
|
|
1316
|
+
"MOUNT_CYCLE",
|
|
1317
|
+
`mount ${mount.path}: the app leads back to this one without stripPrefix`,
|
|
1318
|
+
)
|
|
1319
|
+
}
|
|
1190
1320
|
this.fetchMounts.push(mount)
|
|
1191
1321
|
this.fetchMounts.sort(compareMounts)
|
|
1192
1322
|
this.preRouteMountCount += 1
|
|
@@ -1214,8 +1344,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1214
1344
|
> {
|
|
1215
1345
|
this.assertConfigurable("derive()")
|
|
1216
1346
|
this.derives.push(fn as unknown as RawDerive)
|
|
1217
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1218
|
-
recordScopedHook(this, this.catalog.size, "derive")
|
|
1347
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1348
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "derive")
|
|
1349
|
+
tagHook(fn, "derive", this)
|
|
1350
|
+
}
|
|
1219
1351
|
return this as unknown as Server<
|
|
1220
1352
|
R,
|
|
1221
1353
|
Ctx & ContextExtensionOf<ReturnType<H>>,
|
|
@@ -1252,6 +1384,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1252
1384
|
throw new TypeError('authenticate() stage mode must be "sync" or "async"')
|
|
1253
1385
|
}
|
|
1254
1386
|
this.authStages.push(stage as unknown as RawAuthStage)
|
|
1387
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1388
|
+
tagHook(stage, "authenticate", this)
|
|
1255
1389
|
return this as unknown as Server<R, Ctx & { readonly principal: Principal }, HookOutput>
|
|
1256
1390
|
}
|
|
1257
1391
|
|
|
@@ -1259,19 +1393,21 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1259
1393
|
this.assertConfigurable("decorate()")
|
|
1260
1394
|
this.decorations[key] = value
|
|
1261
1395
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1262
|
-
recordScopedHook(this, this.catalog.size, "decorate")
|
|
1396
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "decorate")
|
|
1263
1397
|
return this as unknown as Server<R, Ctx & Record<K, V>, HookOutput>
|
|
1264
1398
|
}
|
|
1265
1399
|
|
|
1266
1400
|
/**
|
|
1267
|
-
* Run before routing on the raw request. Return a `Response`
|
|
1268
|
-
* `Request` to continue routing with a rewritten method/URL/headers.
|
|
1401
|
+
* Run before routing on the raw request. Return a `Response` or a `status()` result to
|
|
1402
|
+
* short-circuit, or a replacement `Request` to continue routing with a rewritten method/URL/headers.
|
|
1403
|
+
* Global.
|
|
1269
1404
|
*/
|
|
1270
1405
|
onRequest(
|
|
1271
1406
|
fn: (req: Request, platform?: Platform<EnvOf<Ctx>>) => MaybePromise<OnRequestResult>,
|
|
1272
1407
|
): this {
|
|
1273
1408
|
this.assertConfigurable("onRequest()")
|
|
1274
1409
|
this.onRequestHooks.push(fn as RawOnRequest)
|
|
1410
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") tagHook(fn, "onRequest", this)
|
|
1275
1411
|
this.onNodeRequestHooks.push(undefined)
|
|
1276
1412
|
this.nodeRequestHooksComplete = false
|
|
1277
1413
|
this.bunNativeRequestHooksSafe = false
|
|
@@ -1288,8 +1424,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1288
1424
|
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1289
1425
|
this.assertConfigurable("beforeHandle()")
|
|
1290
1426
|
this.beforeHandleHooks.push(fn as unknown as RawBeforeHandle)
|
|
1291
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1292
|
-
recordScopedHook(this, this.catalog.size, "beforeHandle")
|
|
1427
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1428
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "beforeHandle")
|
|
1429
|
+
tagHook(fn, "beforeHandle", this)
|
|
1430
|
+
}
|
|
1293
1431
|
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1294
1432
|
}
|
|
1295
1433
|
|
|
@@ -1306,8 +1444,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1306
1444
|
>(fn: H): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1307
1445
|
this.assertConfigurable("around()")
|
|
1308
1446
|
this.aroundHooks.push(fn as unknown as RawAround)
|
|
1309
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1447
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1310
1448
|
recordScopedHook(this, this.catalog.size, "around")
|
|
1449
|
+
tagHook(fn, "around", this)
|
|
1450
|
+
}
|
|
1311
1451
|
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1312
1452
|
}
|
|
1313
1453
|
|
|
@@ -1355,8 +1495,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1355
1495
|
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1356
1496
|
this.assertConfigurable("afterHandle()")
|
|
1357
1497
|
this.afterHandleHooks.push(fn as unknown as RawAfterHandle)
|
|
1358
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1498
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1359
1499
|
recordScopedHook(this, this.catalog.size, "afterHandle")
|
|
1500
|
+
tagHook(fn, "afterHandle", this)
|
|
1501
|
+
}
|
|
1360
1502
|
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1361
1503
|
}
|
|
1362
1504
|
|
|
@@ -1368,8 +1510,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1368
1510
|
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1369
1511
|
this.assertConfigurable("onError()")
|
|
1370
1512
|
this.onErrorHooks.push(fn as unknown as RawErrorHandler)
|
|
1371
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1372
|
-
recordScopedHook(this, this.catalog.size, "onError")
|
|
1513
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1514
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "onError")
|
|
1515
|
+
tagHook(fn, "onError", this)
|
|
1516
|
+
}
|
|
1373
1517
|
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1374
1518
|
}
|
|
1375
1519
|
|
|
@@ -1377,6 +1521,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1377
1521
|
onResponse(fn: (response: Response, req: Request) => MaybePromise<Response>): this {
|
|
1378
1522
|
this.assertConfigurable("onResponse()")
|
|
1379
1523
|
this.onResponseHooks.push(fn)
|
|
1524
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") tagHook(fn, "onResponse", this)
|
|
1380
1525
|
this.onNodeResponseHooks.push(undefined)
|
|
1381
1526
|
this.nodeResponseHooksComplete = false
|
|
1382
1527
|
this.bunNativeResponseHeadersOnly = false
|
|
@@ -1409,6 +1554,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1409
1554
|
*/
|
|
1410
1555
|
responseHeaders(record: Readonly<Record<string, string>>): this {
|
|
1411
1556
|
this.assertConfigurable("responseHeaders()")
|
|
1557
|
+
if (
|
|
1558
|
+
hookAuditRuntime &&
|
|
1559
|
+
process.env.NODE_ENV !== "production" &&
|
|
1560
|
+
this.onResponseHooks.length === 0
|
|
1561
|
+
)
|
|
1562
|
+
tagStatic(this, Object.keys(record), record)
|
|
1412
1563
|
return this.addStaticResponseHeaders(normalizeStaticResponseHeaders(record))
|
|
1413
1564
|
}
|
|
1414
1565
|
|
|
@@ -1427,13 +1578,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1427
1578
|
})
|
|
1428
1579
|
return this
|
|
1429
1580
|
}
|
|
1430
|
-
// Route the hook through the same seam the static tier uses. On a framework response
|
|
1431
|
-
//
|
|
1432
|
-
//
|
|
1433
|
-
//
|
|
1434
|
-
//
|
|
1581
|
+
// Route the hook through the same seam the static tier uses. On a framework response with a
|
|
1582
|
+
// body it applies the defaults in place and returns the SAME object, so the serialized-body
|
|
1583
|
+
// marker survives for later `onResponseBody`/`onResponseRaw` observers and the Node writer;
|
|
1584
|
+
// reconstructing a `Response` here would strip that marker and reclassify the Node outcome from
|
|
1585
|
+
// `json` to a generic `response`. A body-less or guarded response is copied, marker included.
|
|
1435
1586
|
const statics = buildStaticResponseHeaders(record, this.inheritedStatics)
|
|
1436
|
-
this.onResponseHooks.push((response) =>
|
|
1587
|
+
this.onResponseHooks.push((response, req) =>
|
|
1588
|
+
applyStaticResponseHeaders(response, statics, req),
|
|
1589
|
+
)
|
|
1590
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1591
|
+
tagHook(this.onResponseHooks.at(-1), "responseHeaders", this)
|
|
1437
1592
|
this.onNodeResponseHooks.push(undefined)
|
|
1438
1593
|
this.nodeResponseHooksComplete = false
|
|
1439
1594
|
return this
|
|
@@ -1478,6 +1633,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1478
1633
|
): this {
|
|
1479
1634
|
this.assertConfigurable("onResponseFinalized()")
|
|
1480
1635
|
this.onResponseFinalizedHooks.push(fn)
|
|
1636
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1637
|
+
tagHook(fn, "onResponseFinalized", this)
|
|
1481
1638
|
return this
|
|
1482
1639
|
}
|
|
1483
1640
|
|
|
@@ -1570,9 +1727,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1570
1727
|
this.activeAssurance.push(...pluginOnly)
|
|
1571
1728
|
const inherited = this.inheritedPlugins
|
|
1572
1729
|
this.inheritedPlugins = undefined
|
|
1730
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") enterPlugin(name)
|
|
1573
1731
|
try {
|
|
1574
1732
|
return arg(this)
|
|
1575
1733
|
} finally {
|
|
1734
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") leavePlugin()
|
|
1576
1735
|
this.inheritedPlugins = inherited
|
|
1577
1736
|
// Remove only this plugin's temporary evidence. Nested assured plugins may deliberately leave
|
|
1578
1737
|
// subsequent evidence active, so truncating the whole array would lose real ordering semantics.
|
|
@@ -1602,6 +1761,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1602
1761
|
if (arg.onRequest !== undefined) {
|
|
1603
1762
|
this.assertConfigurable("onRequest()")
|
|
1604
1763
|
this.onRequestHooks.push(arg.onRequest as RawOnRequest)
|
|
1764
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1765
|
+
tagHook(arg.onRequest, "onRequest", this)
|
|
1605
1766
|
this.onNodeRequestHooks.push(arg.onNodeRequest)
|
|
1606
1767
|
if (arg.onNodeRequest === undefined) this.nodeRequestHooksComplete = false
|
|
1607
1768
|
if (!(arg as unknown as Record<symbol, unknown>)[BUN_NATIVE_REQUEST_SAFE]) {
|
|
@@ -1616,6 +1777,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1616
1777
|
if (arg.onResponse !== undefined) {
|
|
1617
1778
|
this.assertConfigurable("onResponse()")
|
|
1618
1779
|
this.onResponseHooks.push(arg.onResponse)
|
|
1780
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1781
|
+
tagHook(arg.onResponse, "onResponse", this)
|
|
1619
1782
|
this.onNodeResponseHooks.push(arg.onNodeResponse)
|
|
1620
1783
|
if (arg.onNodeResponse === undefined) this.nodeResponseHooksComplete = false
|
|
1621
1784
|
else this.hasRawNodeResponseHook = true
|
|
@@ -1672,8 +1835,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1672
1835
|
}
|
|
1673
1836
|
|
|
1674
1837
|
/**
|
|
1675
|
-
* Register a **typed SSE route** -
|
|
1676
|
-
* payloads are contracted by `schema.sse`.
|
|
1838
|
+
* Register a **typed SSE route** - an endpoint streaming `text/event-stream` whose event
|
|
1839
|
+
* payloads are contracted by `schema.sse`. It is a GET route, or a POST route when the schema
|
|
1840
|
+
* declares a `body` (a GET carries none): the body validates as on any POST, and the client sends
|
|
1841
|
+
* it with `.subscribe(onEvent, { body })` - the shape of a chat stream that posts its prompt. The handler receives the validated context plus a
|
|
1677
1842
|
* {@link TypedSSEStream}: `stream.send(event)` is compile-time-checked against the schema and
|
|
1678
1843
|
* JSON-serialized into the SSE `data:` field. The typed client sees the marker and grows a
|
|
1679
1844
|
* `.subscribe(onEvent)` for the route with the same payload type - end-to-end typed streaming.
|
|
@@ -1688,9 +1853,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1688
1853
|
*
|
|
1689
1854
|
* // client: const off = api.feed.subscribe((post) => console.log(post.title))
|
|
1690
1855
|
*
|
|
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.
|
|
1856
|
+
* `init` passes through to the underlying {@link sse} helper (`keepAlive`, `maxBufferedBytes`,
|
|
1857
|
+
* extra headers). The connection closes when the handler resolves, `stream.close()` runs, or the
|
|
1858
|
+
* client disconnects (`stream.signal`). Query/body schemas validate exactly as on any other route.
|
|
1694
1859
|
*/
|
|
1695
1860
|
sse<Path extends string, S extends RouteSchema & { sse: StandardSchemaV1 }>(
|
|
1696
1861
|
path: Path,
|
|
@@ -1701,14 +1866,19 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1701
1866
|
) => void | Promise<void>,
|
|
1702
1867
|
init?: SSEInit,
|
|
1703
1868
|
): Server<
|
|
1704
|
-
AddRoute<R,
|
|
1869
|
+
AddRoute<R, SseMethodOf<S>, Path, RouteInfoFor<Path, S, Response, HookOutput>>,
|
|
1705
1870
|
Ctx,
|
|
1706
1871
|
HookOutput
|
|
1707
1872
|
> {
|
|
1708
1873
|
const handler = (context: Context<Path, S> & Ctx): Response =>
|
|
1709
1874
|
requireSseRuntime(this.sseRuntime).response(context, (stream) => run(context, stream), init)
|
|
1710
|
-
return this.route(
|
|
1711
|
-
|
|
1875
|
+
return this.route(
|
|
1876
|
+
schema.body ? "POST" : "GET",
|
|
1877
|
+
path,
|
|
1878
|
+
schema,
|
|
1879
|
+
handler as unknown as ErasedHandler,
|
|
1880
|
+
) as Server<
|
|
1881
|
+
AddRoute<R, SseMethodOf<S>, Path, RouteInfoFor<Path, S, Response, HookOutput>>,
|
|
1712
1882
|
Ctx,
|
|
1713
1883
|
HookOutput
|
|
1714
1884
|
>
|
|
@@ -1927,8 +2097,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1927
2097
|
|
|
1928
2098
|
/**
|
|
1929
2099
|
* Register a **WebSocket** route. The connection upgrades on a `GET` to `path` carrying
|
|
1930
|
-
* `Upgrade: websocket
|
|
1931
|
-
*
|
|
2100
|
+
* `Upgrade: websocket`. The handshake runs the chain an HTTP `GET` registered here would: the
|
|
2101
|
+
* `authenticate()` stages, `derive`s and `beforeHandle`s added before this call, then the optional
|
|
2102
|
+
* `handler.upgrade(c)`. Any of them may reject (a `Response`, a `status(...)`, a failed auth stage);
|
|
2103
|
+
* `upgrade()` may seed per-connection `ws.data`. To keep a socket public, register it before the
|
|
2104
|
+
* guard. WebSockets are served by the
|
|
1932
2105
|
* adapter (`listen()`, `@nifrajs/node`, `@nifrajs/deno`, `toFetchHandler`) - not by bare `app.fetch`, which
|
|
1933
2106
|
* has no socket (a WS path through `app.fetch` is a normal HTTP response).
|
|
1934
2107
|
*
|
|
@@ -1947,7 +2120,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1947
2120
|
Path extends string = string,
|
|
1948
2121
|
>(
|
|
1949
2122
|
path: Path,
|
|
1950
|
-
handler: WebSocketHandler<Data, EnvOf<Ctx>, Schema, Send>,
|
|
2123
|
+
handler: WebSocketHandler<Data, EnvOf<Ctx>, Schema, Send, Ctx>,
|
|
1951
2124
|
): string extends Path
|
|
1952
2125
|
? Server<R, Ctx, HookOutput>
|
|
1953
2126
|
: Server<AddRoute<R, "WS", Path, WsRouteInfoFor<Path, Schema, Send>>, Ctx, HookOutput> {
|
|
@@ -1967,9 +2140,25 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1967
2140
|
this.topics ??= runtime.createTopics()
|
|
1968
2141
|
// A `messageSchema` wraps `message` with validation once, here - every adapter then dispatches
|
|
1969
2142
|
// already-validated, typed messages (Bun/Deno/Node/Workers) with no per-adapter code.
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
2143
|
+
const wrapped = runtime.wrapHandler(handler as WebSocketHandler, this.protoPoisoning)
|
|
2144
|
+
const upgrade = runtime.upgradeStep(wrapped)
|
|
2145
|
+
for (const form of expandOptionalParams(path)) {
|
|
2146
|
+
// The chain an HTTP `GET` to `form` would capture now, ending in `upgrade()` instead of a
|
|
2147
|
+
// handler. `afterHandle` and `around` are left out: they wrap a response, and an accepted
|
|
2148
|
+
// handshake has none.
|
|
2149
|
+
const { entry, assurance, descriptor } = this.prepareRoute(
|
|
2150
|
+
"WS",
|
|
2151
|
+
form,
|
|
2152
|
+
wrapped.capabilities === undefined ? undefined : { capabilities: wrapped.capabilities },
|
|
2153
|
+
upgrade,
|
|
2154
|
+
)
|
|
2155
|
+
const chain = { ...entry, afterHandle: [] }
|
|
2156
|
+
this.wsRouter.add("GET", form, {
|
|
2157
|
+
handler: wrapped,
|
|
2158
|
+
chain: { ...chain, program: compileRouteProgram({ ...chain, bodySchema: undefined }) },
|
|
2159
|
+
})
|
|
2160
|
+
this.wsRoutes.push({ path: form, capabilities: descriptor.capabilities, assurance, chain })
|
|
2161
|
+
}
|
|
1973
2162
|
wsChanges += 1
|
|
1974
2163
|
return this as never
|
|
1975
2164
|
}
|
|
@@ -2265,30 +2454,6 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2265
2454
|
}
|
|
2266
2455
|
}
|
|
2267
2456
|
|
|
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
2457
|
/** @internal Symbol-keyed install seam for the `responseContract()` plugin. Off the public typed surface. */
|
|
2293
2458
|
[INSTALL_RESPONSE_CONTRACT](runtime: ResponseContractRuntime): void {
|
|
2294
2459
|
this.assertConfigurable("responseContract()")
|
|
@@ -2302,6 +2467,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2302
2467
|
assertConfigurable: (operation) => this.assertConfigurable(operation),
|
|
2303
2468
|
addResponseHook: (web, node, headerOnly = false) => {
|
|
2304
2469
|
this.onResponseHooks.push(web)
|
|
2470
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2471
|
+
tagHook(web, headerOnly ? "responseHeaders" : "onResponse", this)
|
|
2305
2472
|
this.onNodeResponseHooks.push(node)
|
|
2306
2473
|
if (node === undefined) this.nodeResponseHooksComplete = false
|
|
2307
2474
|
if (!headerOnly) this.bunNativeResponseHeadersOnly = false
|
|
@@ -2343,6 +2510,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2343
2510
|
this.nodeOutcomeRuntime = runtime
|
|
2344
2511
|
}
|
|
2345
2512
|
|
|
2513
|
+
/** @internal An adapter reports a failure no response hook saw (a hook that threw past `fetch`, a
|
|
2514
|
+
* response the runtime refused to write), logged as Bun's `listen()` logs one. */
|
|
2515
|
+
[REPORT_ERROR](error: unknown): void {
|
|
2516
|
+
emitRequestErrorLog(this.logger, this.errorLogDetail, error, undefined)
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2346
2519
|
/** @internal Symbol-keyed install seam for the `streaming()` plugin. Off the public typed surface. */
|
|
2347
2520
|
[INSTALL_SSE](runtime: SseRuntime): void {
|
|
2348
2521
|
this.assertConfigurable("streaming()")
|
|
@@ -2363,7 +2536,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2363
2536
|
* counts. `seen` ends a mount cycle (an app mounted under itself to alias a prefix).
|
|
2364
2537
|
*/
|
|
2365
2538
|
[GET_WS_RUNTIME](seen?: Set<unknown>): WsRuntime | null | undefined {
|
|
2366
|
-
return this.
|
|
2539
|
+
return this.wsRoutes.length > 0 ? this.wsRuntime : this.mountedWsRuntime(seen)
|
|
2367
2540
|
}
|
|
2368
2541
|
|
|
2369
2542
|
/** The `[GET_WS_RUNTIME]` answer for the apps mounted here alone, whatever this app's own routes. */
|
|
@@ -2404,15 +2577,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2404
2577
|
* `derive`/`decorate`/`beforeHandle`/`afterHandle`/`onError`/`around` apply to its routes
|
|
2405
2578
|
* exactly as they did standalone, so a group wires its own plugins. The same locality rule
|
|
2406
2579
|
* 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
|
|
2580
|
+
* 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
2581
|
* because the app composed it in), and the group's global assurance follows its hooks onto
|
|
2409
2582
|
* exactly those routes. A group with hooks but NO routes is a middleware bundle - its hooks
|
|
2410
2583
|
* can only mean app-wide intent, so they are appended globally, unchanged. Response-side hooks
|
|
2411
2584
|
* (`onResponse`/`onResponseFinalized`) are appended to this server's. This server's
|
|
2412
2585
|
* route-scoped chains do NOT retroactively wrap merged routes (order-scoped, like routes
|
|
2413
2586
|
* 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).
|
|
2587
|
+
* `RouteConfigError` at merge time, and a group with WebSocket routes or mounts is refused
|
|
2588
|
+
* (register those on the parent).
|
|
2416
2589
|
*/
|
|
2417
2590
|
merge<R2 extends Registry, Ctx2, HookOutput2>(
|
|
2418
2591
|
other: Server<R2, Ctx2, HookOutput2>,
|
|
@@ -2432,10 +2605,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2432
2605
|
)
|
|
2433
2606
|
}
|
|
2434
2607
|
const source = other as unknown as Server<Registry, EmptyContext>
|
|
2435
|
-
if (source.
|
|
2608
|
+
if (source.wsRoutes.length > 0 || source.fetchMounts.length > 0) {
|
|
2436
2609
|
throw new RouteConfigError(
|
|
2437
2610
|
"INVALID_PATH",
|
|
2438
|
-
"merge() does not carry WebSocket routes - register
|
|
2611
|
+
"merge() does not carry WebSocket routes or mounts - register them on the parent server",
|
|
2439
2612
|
)
|
|
2440
2613
|
}
|
|
2441
2614
|
this.adopt(source, undefined, 0)
|
|
@@ -2521,7 +2694,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2521
2694
|
throw new TypeError(`${label} builder must return its group synchronously`)
|
|
2522
2695
|
}
|
|
2523
2696
|
if (
|
|
2524
|
-
scope.
|
|
2697
|
+
scope.wsRoutes.length > 0 ||
|
|
2525
2698
|
scope.fetchMounts.length > 0 ||
|
|
2526
2699
|
scope.catalog.entries().some((route) => route.descriptor.tool !== undefined)
|
|
2527
2700
|
) {
|
|
@@ -2531,7 +2704,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2531
2704
|
)
|
|
2532
2705
|
}
|
|
2533
2706
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2534
|
-
sealHookAudit(scope, scope.catalog.size, this.logger)
|
|
2707
|
+
sealHookAudit(scope, scope.catalog.size, scope.wsRoutes.length, this.logger)
|
|
2535
2708
|
this.adopt(scope, fullPrefix, this.globalAssurance.length)
|
|
2536
2709
|
return this as unknown as Server<R & PrefixRegistry<P, R2>, Ctx, HookOutput>
|
|
2537
2710
|
}
|
|
@@ -2593,12 +2766,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2593
2766
|
// The group's static declarations came before its own response hooks, so they are folded in
|
|
2594
2767
|
// first - and fold themselves into a hook here if this server already has one (same ordering
|
|
2595
2768
|
// rule as a direct `responseHeaders()` call).
|
|
2769
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2770
|
+
adoptStaticTags(this, source, this.onResponseHooks.length > 0)
|
|
2596
2771
|
this.addStaticResponseHeaders({ ...statics.record })
|
|
2597
2772
|
} else {
|
|
2598
2773
|
// The static tier is folded into EVERY response this server builds, so a group's static
|
|
2599
2774
|
// headers cannot join it. A hook ahead of the group's own (the sealed scope is discarded
|
|
2600
2775
|
// after adoption) keeps their meaning - defaults a value already on the response wins over.
|
|
2601
|
-
source.onResponseHooks.unshift((response) =>
|
|
2776
|
+
source.onResponseHooks.unshift((response, req) =>
|
|
2777
|
+
applyStaticResponseHeaders(response, statics, req),
|
|
2778
|
+
)
|
|
2779
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2780
|
+
tagHook(source.onResponseHooks[0], "responseHeaders", source, true)
|
|
2602
2781
|
source.onNodeResponseHooks.unshift(undefined)
|
|
2603
2782
|
source.nodeResponseHooksComplete = false
|
|
2604
2783
|
}
|
|
@@ -2608,6 +2787,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2608
2787
|
this.onResponseFinalizedHooks.push(
|
|
2609
2788
|
...scopeHooks(source.onResponseFinalizedHooks, responseScope, 1),
|
|
2610
2789
|
)
|
|
2790
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
2791
|
+
// Read before anything else is appended: the wrappers are still each list's tail.
|
|
2792
|
+
const pairs: (readonly [readonly unknown[], readonly unknown[]])[] = []
|
|
2793
|
+
if (inScope !== undefined) pairs.push([source.onRequestHooks, this.onRequestHooks])
|
|
2794
|
+
if (responseScope !== undefined) {
|
|
2795
|
+
pairs.push([source.onResponseHooks, this.onResponseHooks])
|
|
2796
|
+
pairs.push([source.onResponseFinalizedHooks, this.onResponseFinalizedHooks])
|
|
2797
|
+
}
|
|
2798
|
+
tagAdopted(this, source, prefix, sourceRoutes, pairs)
|
|
2799
|
+
}
|
|
2611
2800
|
this.nodeResponseHooksComplete &&= source.nodeResponseHooksComplete
|
|
2612
2801
|
this.bunNativeResponseHeadersOnly &&= source.bunNativeResponseHeadersOnly
|
|
2613
2802
|
this.hasRawNodeResponseHook ||= source.hasRawNodeResponseHook
|
|
@@ -2799,7 +2988,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2799
2988
|
// Seal once on the first request so test servers (no `listen()`) still get the dead-hook check.
|
|
2800
2989
|
// After that it is a single boolean read - the per-request cost the option promises to keep at zero.
|
|
2801
2990
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2802
|
-
sealHookAudit(this, this.catalog.size, this.logger)
|
|
2991
|
+
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger)
|
|
2803
2992
|
// Off path (default): straight through - one property check, no closure, no promise.
|
|
2804
2993
|
if (this.capacityGate === undefined) return this.fetchSourceInner(source, platform)
|
|
2805
2994
|
return this.admitGated(requestOf(source), () => this.fetchSourceInner(source, platform))
|
|
@@ -2983,7 +3172,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2983
3172
|
): MaybePromise<NodeServeOutcome> {
|
|
2984
3173
|
// Seal once on the Node-direct lane too - it can bypass `fetchSource` entirely (hookless outcome).
|
|
2985
3174
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2986
|
-
sealHookAudit(this, this.catalog.size, this.logger)
|
|
3175
|
+
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger)
|
|
2987
3176
|
// A paired header-only native hook can preserve the Node-direct JSON/body outcome for successful
|
|
2988
3177
|
// responses. Arbitrary onResponse transforms need a real Web Response, but a buffered outcome can
|
|
2989
3178
|
// be materialized with a direct-write marker and return to the socket path when the hook mutates
|
|
@@ -3242,7 +3431,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3242
3431
|
current = outcome
|
|
3243
3432
|
continue
|
|
3244
3433
|
}
|
|
3245
|
-
if (outcome !== undefined) return outcome
|
|
3434
|
+
if (outcome !== undefined) return webResponseOf(outcome)
|
|
3246
3435
|
}
|
|
3247
3436
|
return current
|
|
3248
3437
|
}
|
|
@@ -3257,7 +3446,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3257
3446
|
let outcome = first
|
|
3258
3447
|
for (let i = start; ; i++) {
|
|
3259
3448
|
if (outcome instanceof Request) next = outcome
|
|
3260
|
-
else if (outcome !== undefined) return outcome
|
|
3449
|
+
else if (outcome !== undefined) return webResponseOf(outcome)
|
|
3261
3450
|
if (i >= this.onRequestHooks.length) return next
|
|
3262
3451
|
const hook = this.onRequestHooks[i]
|
|
3263
3452
|
if (hook === undefined) return next
|
|
@@ -3271,11 +3460,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3271
3460
|
): MaybePromise<WebSocketUpgradeOutcome> {
|
|
3272
3461
|
// Before any header is read: Deno only materializes a request's headers when asked, and that
|
|
3273
3462
|
// costs a plain request measurably.
|
|
3274
|
-
if (this.
|
|
3463
|
+
if (this.wsRoutes.length === 0 && !this.mountsTakeUpgrades()) return WS_PASS
|
|
3275
3464
|
if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") return WS_PASS
|
|
3276
3465
|
// A WebSocket handshake is a GET (RFC 6455 section 4.1). Any other method with an Upgrade header
|
|
3277
3466
|
// is an ordinary request: its HTTP route answers it, and normal routing refuses an unknown token.
|
|
3278
3467
|
if (req.method !== "GET") return WS_PASS
|
|
3468
|
+
// The adapter answers a pass by handing this same object to `fetch`.
|
|
3469
|
+
const received = req
|
|
3279
3470
|
// The handshake routes the path `fetch` would: dot segments resolved (see `fetch`).
|
|
3280
3471
|
if (rawRequestTargets && hasDotSegment(req.url)) req = withResolvedTarget(req)
|
|
3281
3472
|
// The handshake's caller is the one `fetch` derives: hooks and `upgrade()` see the trusted IP.
|
|
@@ -3290,11 +3481,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3290
3481
|
const controller = timeoutMs > 0 ? new AbortController() : undefined
|
|
3291
3482
|
const signal = controller?.signal ?? getNeverAbortSignal()
|
|
3292
3483
|
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : undefined
|
|
3293
|
-
|
|
3294
|
-
const
|
|
3484
|
+
// A refusal carries the static response headers an HTTP answer would; a guard's already does.
|
|
3485
|
+
const refuse = (response: Response): WebSocketUpgradeOutcome => ({
|
|
3295
3486
|
kind: "reject",
|
|
3296
|
-
response:
|
|
3487
|
+
response: this.wrapWebResponse(response),
|
|
3297
3488
|
})
|
|
3489
|
+
const onTimeout = (): WebSocketUpgradeOutcome => refuse(jsonError(503, "request_timeout"))
|
|
3298
3490
|
const bounded = <T>(work: Promise<T>, onDone: () => T): Promise<T> =>
|
|
3299
3491
|
controller === undefined
|
|
3300
3492
|
? work
|
|
@@ -3307,11 +3499,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3307
3499
|
// Inspect only captured values for escapes. Scanning the full pathname repeated work the router
|
|
3308
3500
|
// already did and made every plain dynamic route pay for unrelated static path bytes.
|
|
3309
3501
|
const params = match.params === EMPTY_PARAMS ? match.params : decodeRouteParams(match.params)
|
|
3310
|
-
if (params === null) return
|
|
3311
|
-
const handler = match.payload
|
|
3312
|
-
// Non-null:
|
|
3502
|
+
if (params === null) return refuse(jsonError(400, "malformed_path"))
|
|
3503
|
+
const { handler, chain } = match.payload
|
|
3504
|
+
// Non-null: a socket route ⇒ ws() ran ⇒ `.use(websocket())` installed the runtime + registry.
|
|
3313
3505
|
const pubsub = this.topics as TopicRegistry
|
|
3314
|
-
const
|
|
3506
|
+
const runtime = this.wsRuntime as WsRuntime
|
|
3315
3507
|
// CSWSH guard, before any per-connection work or the user's upgrade(): reject a disallowed
|
|
3316
3508
|
// Origin with 403. Browsers don't CORS-protect WS handshakes but do send cookies, so this
|
|
3317
3509
|
// blocks cross-site authenticated sockets when the route opts in via `allowedOrigins`.
|
|
@@ -3321,23 +3513,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3321
3513
|
typeof handler.allowedOrigins === "function"
|
|
3322
3514
|
? handler.allowedOrigins(origin)
|
|
3323
3515
|
: origin !== null && handler.allowedOrigins.includes(origin)
|
|
3324
|
-
if (!allowed) return
|
|
3516
|
+
if (!allowed) return refuse(jsonError(403, "forbidden_origin"))
|
|
3325
3517
|
} else if (origin !== null && !wsSameOrigin(origin, request)) {
|
|
3326
3518
|
// Secure default (no explicit `allowedOrigins`): reject a CROSS-ORIGIN browser handshake - the
|
|
3327
3519
|
// CSWSH case, since browsers send cookies on WS handshakes and don't apply CORS. Non-browser
|
|
3328
3520
|
// clients send no `Origin` and pass; same-origin browsers pass. Set `allowedOrigins` to permit
|
|
3329
3521
|
// 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
|
-
}
|
|
3522
|
+
return refuse(jsonError(403, "forbidden_origin"))
|
|
3341
3523
|
}
|
|
3342
3524
|
const budget =
|
|
3343
3525
|
controller === undefined
|
|
@@ -3353,28 +3535,31 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3353
3535
|
this.maxBodyBytes,
|
|
3354
3536
|
this.protoPoisoning,
|
|
3355
3537
|
)
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3538
|
+
// Rendered as the same chain on an HTTP `GET` renders its early results.
|
|
3539
|
+
const finalize = (result: unknown, set: CtxSet): WebSocketUpgradeOutcome => {
|
|
3540
|
+
const accepted = runtime.accepted(result)
|
|
3541
|
+
return accepted === undefined
|
|
3542
|
+
? { kind: "reject", response: this.finalizeResponse(result, set) }
|
|
3359
3543
|
: {
|
|
3360
3544
|
kind: "upgrade",
|
|
3361
3545
|
handler,
|
|
3362
|
-
data:
|
|
3546
|
+
data: accepted.data,
|
|
3363
3547
|
pubsub,
|
|
3364
|
-
attach,
|
|
3548
|
+
attach: runtime.attach,
|
|
3365
3549
|
maxPayloadBytes: this.wsMaxPayloadBytes,
|
|
3366
3550
|
}
|
|
3551
|
+
}
|
|
3552
|
+
const wrap = (response: Response | ResponseResult): WebSocketUpgradeOutcome =>
|
|
3553
|
+
finalize(response, EMPTY_RESPONSE_CONTROLS)
|
|
3554
|
+
let outcome: MaybePromise<WebSocketUpgradeOutcome>
|
|
3367
3555
|
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)
|
|
3556
|
+
outcome = executeRouteProgram(this, chain, chain.program, request, ctx, finalize, wrap)
|
|
3375
3557
|
} catch {
|
|
3376
|
-
return
|
|
3558
|
+
return refuse(jsonError(500, "internal_error"))
|
|
3377
3559
|
}
|
|
3560
|
+
if (!(outcome instanceof Promise)) return outcome
|
|
3561
|
+
const pending = outcome.then(undefined, () => refuse(jsonError(500, "internal_error")))
|
|
3562
|
+
return bounded(pending, onTimeout)
|
|
3378
3563
|
}
|
|
3379
3564
|
|
|
3380
3565
|
const resolveMounted = (request: Request): MaybePromise<WebSocketUpgradeOutcome> => {
|
|
@@ -3397,20 +3582,25 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3397
3582
|
}
|
|
3398
3583
|
}
|
|
3399
3584
|
if (selected === undefined) return WS_PASS
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3585
|
+
let childRequest = request
|
|
3586
|
+
if (selected.stripPrefix) {
|
|
3587
|
+
childRequest = stripMountPrefix(request, selected.path)
|
|
3588
|
+
selected.wsPassed ??= new WeakMap()
|
|
3589
|
+
selected.wsPassed.set(request, childRequest)
|
|
3590
|
+
}
|
|
3403
3591
|
let child: MaybePromise<WebSocketUpgradeOutcome>
|
|
3404
3592
|
try {
|
|
3405
3593
|
child = selected.resolveWebSocketUpgrade!(childRequest, platform)
|
|
3406
3594
|
} catch {
|
|
3407
|
-
return
|
|
3595
|
+
return refuse(jsonError(500, "internal_error"))
|
|
3408
3596
|
}
|
|
3409
3597
|
const settle = (outcome: WebSocketUpgradeOutcome): MaybePromise<WebSocketUpgradeOutcome> =>
|
|
3410
3598
|
outcome.kind === "pass" ||
|
|
3411
3599
|
(outcome.kind === "reject" && outcome.response.status === 404 && selected!.fallbackOn404)
|
|
3412
3600
|
? dispatch(selectedIndex + 1)
|
|
3413
|
-
: outcome
|
|
3601
|
+
: outcome.kind === "reject"
|
|
3602
|
+
? refuse(outcome.response)
|
|
3603
|
+
: outcome
|
|
3414
3604
|
return child instanceof Promise ? child.then(settle) : settle(child)
|
|
3415
3605
|
}
|
|
3416
3606
|
return dispatch(0)
|
|
@@ -3432,23 +3622,25 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3432
3622
|
}
|
|
3433
3623
|
|
|
3434
3624
|
if (this.onRequestHooks.length === 0) return resolveWithDeadline(req)
|
|
3625
|
+
// On a pass the adapter hands `received` to `fetch`, which then routes these results. An upgrade
|
|
3626
|
+
// or a rejection never reaches `fetch`, so the entry dies with the request.
|
|
3627
|
+
const resolveHooked = (request: Request): MaybePromise<WebSocketUpgradeOutcome> => {
|
|
3628
|
+
this.wsHookedPasses ??= new WeakMap()
|
|
3629
|
+
this.wsHookedPasses.set(received, request)
|
|
3630
|
+
return resolveWithDeadline(request)
|
|
3631
|
+
}
|
|
3435
3632
|
let hooked: MaybePromise<Request | Response>
|
|
3436
3633
|
try {
|
|
3437
3634
|
hooked = this.runWebSocketRequestHooks(req, platform)
|
|
3438
3635
|
} catch {
|
|
3439
|
-
return
|
|
3636
|
+
return refuse(jsonError(500, "internal_error"))
|
|
3440
3637
|
}
|
|
3441
3638
|
if (!(hooked instanceof Promise)) {
|
|
3442
|
-
return hooked instanceof Response
|
|
3443
|
-
? { kind: "reject", response: hooked }
|
|
3444
|
-
: resolveWithDeadline(hooked)
|
|
3639
|
+
return hooked instanceof Response ? refuse(hooked) : resolveHooked(hooked)
|
|
3445
3640
|
}
|
|
3446
3641
|
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") }),
|
|
3642
|
+
(result) => (result instanceof Response ? refuse(result) : resolveHooked(result)),
|
|
3643
|
+
() => refuse(jsonError(500, "internal_error")),
|
|
3452
3644
|
)
|
|
3453
3645
|
return bounded(
|
|
3454
3646
|
Promise.resolve(pending).then((result) => result),
|
|
@@ -3512,7 +3704,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3512
3704
|
private dispatch<T>(
|
|
3513
3705
|
source: RequestSource,
|
|
3514
3706
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3515
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3707
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3516
3708
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3517
3709
|
onTimeout: () => T,
|
|
3518
3710
|
// True only from the Web `fetch` path - unlocks each route's fused lane, whose output type IS
|
|
@@ -3532,6 +3724,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3532
3724
|
if (this.onRequestHooks.length === 0 || !isRoutableMethod(source.method)) {
|
|
3533
3725
|
return this.routeAndRun(source, resolved, finalize, wrapResponse, onTimeout, webFast)
|
|
3534
3726
|
}
|
|
3727
|
+
const hooked = this.wsHookedPasses?.get(source)
|
|
3728
|
+
if (hooked !== undefined) {
|
|
3729
|
+
const request = requestOf(source)
|
|
3730
|
+
this.wsHookedPasses?.delete(request)
|
|
3731
|
+
this.responseRequests.set(request, hooked)
|
|
3732
|
+
return this.routeAndRun(hooked, resolved, finalize, wrapResponse, onTimeout, webFast)
|
|
3733
|
+
}
|
|
3535
3734
|
if (!webFast && this.canUseNodeRequestHooks()) {
|
|
3536
3735
|
return this.runWithNodeRequest(source, resolved, finalize, wrapResponse, onTimeout)
|
|
3537
3736
|
}
|
|
@@ -3543,7 +3742,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3543
3742
|
private runWithNodeRequest<T>(
|
|
3544
3743
|
source: RequestSource,
|
|
3545
3744
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3546
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3745
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3547
3746
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3548
3747
|
onTimeout: () => T,
|
|
3549
3748
|
): MaybePromise<T> {
|
|
@@ -3577,7 +3776,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3577
3776
|
request: NodeRequestContext,
|
|
3578
3777
|
source: RequestSource,
|
|
3579
3778
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3580
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3779
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3581
3780
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3582
3781
|
onTimeout: () => T,
|
|
3583
3782
|
): Promise<T> {
|
|
@@ -3605,7 +3804,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3605
3804
|
private runWithOnRequest<T>(
|
|
3606
3805
|
source: RequestSource,
|
|
3607
3806
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3608
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3807
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3609
3808
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3610
3809
|
onTimeout: () => T,
|
|
3611
3810
|
webFast: boolean,
|
|
@@ -3654,7 +3853,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3654
3853
|
originalRequest: Request,
|
|
3655
3854
|
sourceAtAwait: RequestSource,
|
|
3656
3855
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3657
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3856
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3658
3857
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3659
3858
|
onTimeout: () => T,
|
|
3660
3859
|
webFast: boolean,
|
|
@@ -3751,7 +3950,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3751
3950
|
private routeAndRun<T>(
|
|
3752
3951
|
source: RequestSource,
|
|
3753
3952
|
platform: Platform<EnvOf<Ctx>> | undefined,
|
|
3754
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
3953
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3755
3954
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3756
3955
|
onTimeout: () => T,
|
|
3757
3956
|
webFast: boolean,
|
|
@@ -3881,7 +4080,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3881
4080
|
}
|
|
3882
4081
|
}
|
|
3883
4082
|
if (mount === undefined) return fallback ?? jsonError(404, "not_found")
|
|
3884
|
-
const childRequest = mount.stripPrefix
|
|
4083
|
+
const childRequest = mount.stripPrefix
|
|
4084
|
+
? (mount.wsPassed?.get(request) ?? stripMountPrefix(request, mount.path))
|
|
4085
|
+
: request
|
|
3885
4086
|
const outcome = mount.handler(childRequest, platform)
|
|
3886
4087
|
if (!replayable || !mount.fallbackOn404) return outcome
|
|
3887
4088
|
if (outcome instanceof Promise) {
|
|
@@ -3901,7 +4102,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3901
4102
|
entry: RouteEntry,
|
|
3902
4103
|
params: Record<string, string>,
|
|
3903
4104
|
search: string | undefined,
|
|
3904
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4105
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3905
4106
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3906
4107
|
onTimeout: () => T,
|
|
3907
4108
|
webFast: boolean,
|
|
@@ -3939,7 +4140,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3939
4140
|
{
|
|
3940
4141
|
maxBodyBytes: entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3941
4142
|
runLanes: (buffered, plat, ent, prm, srch) =>
|
|
3942
|
-
this.
|
|
4143
|
+
this.runMatchedLanes(
|
|
4144
|
+
buffered,
|
|
4145
|
+
plat,
|
|
4146
|
+
ent as RouteEntry,
|
|
4147
|
+
prm,
|
|
4148
|
+
srch,
|
|
4149
|
+
this.finalizeResponse,
|
|
4150
|
+
this.wrapWebResponse,
|
|
4151
|
+
this.webResponseTimeout,
|
|
4152
|
+
false,
|
|
4153
|
+
),
|
|
3943
4154
|
},
|
|
3944
4155
|
)
|
|
3945
4156
|
}
|
|
@@ -3963,7 +4174,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3963
4174
|
entry: RouteEntry,
|
|
3964
4175
|
params: Record<string, string>,
|
|
3965
4176
|
search: string | undefined,
|
|
3966
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4177
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
3967
4178
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3968
4179
|
onTimeout: () => T,
|
|
3969
4180
|
webFast: boolean,
|
|
@@ -4004,32 +4215,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4004
4215
|
: createRequestBudget({ deadline: admission!.deadline as number, signal })
|
|
4005
4216
|
const plan = entry.execution
|
|
4006
4217
|
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,
|
|
4218
|
+
let outcome: MaybePromise<T>
|
|
4219
|
+
try {
|
|
4220
|
+
outcome =
|
|
4221
|
+
webFast && plan.fusedWeb !== undefined
|
|
4222
|
+
? (plan.fusedWeb(
|
|
4033
4223
|
source,
|
|
4034
4224
|
params,
|
|
4035
4225
|
search,
|
|
@@ -4037,9 +4227,46 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4037
4227
|
budget,
|
|
4038
4228
|
platform,
|
|
4039
4229
|
nativeContext,
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4230
|
+
) as MaybePromise<T>)
|
|
4231
|
+
: !webFast && plan.fusedBody !== undefined
|
|
4232
|
+
? plan.fusedBody(
|
|
4233
|
+
source,
|
|
4234
|
+
params,
|
|
4235
|
+
search,
|
|
4236
|
+
signal,
|
|
4237
|
+
budget,
|
|
4238
|
+
platform,
|
|
4239
|
+
false,
|
|
4240
|
+
finalize,
|
|
4241
|
+
wrapResponse,
|
|
4242
|
+
)
|
|
4243
|
+
: plan.run(
|
|
4244
|
+
this,
|
|
4245
|
+
entry,
|
|
4246
|
+
source,
|
|
4247
|
+
params,
|
|
4248
|
+
search,
|
|
4249
|
+
signal,
|
|
4250
|
+
budget,
|
|
4251
|
+
platform,
|
|
4252
|
+
nativeContext,
|
|
4253
|
+
finalize,
|
|
4254
|
+
wrapResponse,
|
|
4255
|
+
)
|
|
4256
|
+
} catch (err) {
|
|
4257
|
+
// A lane's synchronous render throw (a BigInt, a header value with a line break): the lanes
|
|
4258
|
+
// guard only their async continuations, since a `try` around a fused render slows it.
|
|
4259
|
+
return this.contextlessBareError(
|
|
4260
|
+
err,
|
|
4261
|
+
source,
|
|
4262
|
+
params,
|
|
4263
|
+
search,
|
|
4264
|
+
signal,
|
|
4265
|
+
budget,
|
|
4266
|
+
platform,
|
|
4267
|
+
wrapResponse,
|
|
4268
|
+
)
|
|
4269
|
+
}
|
|
4043
4270
|
// The request timeout only bounds work that is actually pending - a synchronous (bare) result is
|
|
4044
4271
|
// already complete and can't time out, so it's returned as-is (no 503 race, no promise).
|
|
4045
4272
|
if (controller !== undefined && outcome instanceof Promise) {
|
|
@@ -4074,7 +4301,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4074
4301
|
signal: AbortSignal,
|
|
4075
4302
|
budget: RequestBudget,
|
|
4076
4303
|
platform: Platform | undefined,
|
|
4077
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4304
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
4078
4305
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4079
4306
|
): MaybePromise<T> {
|
|
4080
4307
|
let result: unknown
|
|
@@ -4094,7 +4321,22 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4094
4321
|
}
|
|
4095
4322
|
if (result instanceof Promise) {
|
|
4096
4323
|
return result.then(
|
|
4097
|
-
(value) =>
|
|
4324
|
+
(value) => {
|
|
4325
|
+
try {
|
|
4326
|
+
return finalize(value, EMPTY_RESPONSE_CONTROLS)
|
|
4327
|
+
} catch (err) {
|
|
4328
|
+
return this.contextlessBareError(
|
|
4329
|
+
err,
|
|
4330
|
+
source,
|
|
4331
|
+
params,
|
|
4332
|
+
search,
|
|
4333
|
+
signal,
|
|
4334
|
+
budget,
|
|
4335
|
+
platform,
|
|
4336
|
+
wrapResponse,
|
|
4337
|
+
)
|
|
4338
|
+
}
|
|
4339
|
+
},
|
|
4098
4340
|
(err) =>
|
|
4099
4341
|
this.contextlessBareError(
|
|
4100
4342
|
err,
|
|
@@ -4151,7 +4393,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4151
4393
|
private runBare<T>(
|
|
4152
4394
|
entry: RouteEntry,
|
|
4153
4395
|
ctx: RawContext,
|
|
4154
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
4396
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
4155
4397
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4156
4398
|
): MaybePromise<T> {
|
|
4157
4399
|
let result: unknown
|
|
@@ -4164,11 +4406,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4164
4406
|
if (result instanceof Promise) {
|
|
4165
4407
|
// Async handler on an otherwise-bare route: finish on a microtask, with the same error handling.
|
|
4166
4408
|
return result.then(
|
|
4167
|
-
(value) =>
|
|
4409
|
+
(value) => {
|
|
4410
|
+
// A synchronous finalize throw reaches runMatchedLanes; this one would reject.
|
|
4411
|
+
try {
|
|
4412
|
+
return finalize(value, responseSet(ctx), ctx)
|
|
4413
|
+
} catch (err) {
|
|
4414
|
+
return this.bareError(err, ctx, finalize, wrapResponse)
|
|
4415
|
+
}
|
|
4416
|
+
},
|
|
4168
4417
|
(err) => this.bareError(err, ctx, finalize, wrapResponse),
|
|
4169
4418
|
)
|
|
4170
4419
|
}
|
|
4171
|
-
return finalize(result, responseSet(ctx))
|
|
4420
|
+
return finalize(result, responseSet(ctx), ctx)
|
|
4172
4421
|
}
|
|
4173
4422
|
|
|
4174
4423
|
/** Bare-route error rendering - identical to the compiled route program's catch minus the (absent) onError
|
|
@@ -4200,66 +4449,62 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4200
4449
|
// `() => ...` can't observe the context - skip allocating one entirely (errors still build
|
|
4201
4450
|
// one for the structured log, exactly like runContextlessBare).
|
|
4202
4451
|
const contextlessHandler = handler as unknown as ContextlessHandler
|
|
4452
|
+
const fail = (
|
|
4453
|
+
err: unknown,
|
|
4454
|
+
source: RequestSource,
|
|
4455
|
+
params: Record<string, string>,
|
|
4456
|
+
search: string | undefined,
|
|
4457
|
+
signal: AbortSignal,
|
|
4458
|
+
budget: RequestBudget,
|
|
4459
|
+
platform: Platform | undefined,
|
|
4460
|
+
nativeContext: boolean,
|
|
4461
|
+
): Response =>
|
|
4462
|
+
logError(
|
|
4463
|
+
err,
|
|
4464
|
+
nativeContext
|
|
4465
|
+
? RequestContext.native(
|
|
4466
|
+
source,
|
|
4467
|
+
params,
|
|
4468
|
+
search,
|
|
4469
|
+
maxBodyBytes,
|
|
4470
|
+
platform,
|
|
4471
|
+
this.protoPoisoning,
|
|
4472
|
+
)
|
|
4473
|
+
: new RequestContext(
|
|
4474
|
+
source,
|
|
4475
|
+
params,
|
|
4476
|
+
search,
|
|
4477
|
+
signal,
|
|
4478
|
+
budget,
|
|
4479
|
+
platform,
|
|
4480
|
+
maxBodyBytes,
|
|
4481
|
+
this.protoPoisoning,
|
|
4482
|
+
),
|
|
4483
|
+
)
|
|
4203
4484
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
4204
4485
|
let result: unknown
|
|
4205
4486
|
try {
|
|
4206
4487
|
result = contextlessHandler()
|
|
4207
4488
|
} 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
|
-
)
|
|
4489
|
+
return fail(err, source, params, search, signal, budget, platform, nativeContext)
|
|
4230
4490
|
}
|
|
4231
4491
|
if (result instanceof Promise) {
|
|
4232
4492
|
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
|
-
),
|
|
4493
|
+
(value) => {
|
|
4494
|
+
// A synchronous render throw reaches runMatchedLanes; this one would reject.
|
|
4495
|
+
try {
|
|
4496
|
+
return fusedRespondNoSet(
|
|
4497
|
+
value,
|
|
4498
|
+
this.responseBodyTag,
|
|
4499
|
+
this.staticResponseHeaders,
|
|
4500
|
+
this.onResponseHooks.length === 0,
|
|
4501
|
+
this.onResponseHooks.length === 0 ? undefined : requestOf(source),
|
|
4502
|
+
)
|
|
4503
|
+
} catch (err) {
|
|
4504
|
+
return fail(err, source, params, search, signal, budget, platform, nativeContext)
|
|
4505
|
+
}
|
|
4506
|
+
},
|
|
4507
|
+
(err) => fail(err, source, params, search, signal, budget, platform, nativeContext),
|
|
4263
4508
|
)
|
|
4264
4509
|
}
|
|
4265
4510
|
return fusedRespondNoSet(
|
|
@@ -4267,6 +4512,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4267
4512
|
this.responseBodyTag,
|
|
4268
4513
|
this.staticResponseHeaders,
|
|
4269
4514
|
this.onResponseHooks.length === 0,
|
|
4515
|
+
this.onResponseHooks.length === 0 ? undefined : requestOf(source),
|
|
4270
4516
|
)
|
|
4271
4517
|
}
|
|
4272
4518
|
}
|
|
@@ -4292,14 +4538,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4292
4538
|
}
|
|
4293
4539
|
if (result instanceof Promise) {
|
|
4294
4540
|
return result.then(
|
|
4295
|
-
(value) =>
|
|
4296
|
-
fusedRespond(
|
|
4297
|
-
value,
|
|
4298
|
-
ctx,
|
|
4299
|
-
this.responseBodyTag,
|
|
4300
|
-
this.staticResponseHeaders,
|
|
4301
|
-
this.onResponseHooks.length === 0,
|
|
4302
|
-
),
|
|
4541
|
+
(value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0),
|
|
4303
4542
|
(err) => logError(err, ctx),
|
|
4304
4543
|
)
|
|
4305
4544
|
}
|
|
@@ -4313,6 +4552,21 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4313
4552
|
}
|
|
4314
4553
|
}
|
|
4315
4554
|
|
|
4555
|
+
/** A fused Web lane's render in an async continuation, where a result that cannot be rendered
|
|
4556
|
+
* must fail like a handler throw; a synchronous render throw reaches {@link runMatchedLanes}. */
|
|
4557
|
+
private respondFused(result: unknown, ctx: RawContext, unstamped?: boolean): Response {
|
|
4558
|
+
try {
|
|
4559
|
+
return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, unstamped)
|
|
4560
|
+
} catch (err) {
|
|
4561
|
+
return this.bareError(
|
|
4562
|
+
err,
|
|
4563
|
+
ctx,
|
|
4564
|
+
(value, set) => this.wrapWebResponse(toResponse(value, set)),
|
|
4565
|
+
this.wrapWebResponse,
|
|
4566
|
+
)
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4316
4570
|
/** Compile the eligible body-only route's parser → validator → handler continuation once. The
|
|
4317
4571
|
* bounded parser remains shared with the generic lane; only the route-invariant entry lookups and
|
|
4318
4572
|
* lifecycle dispatch disappear from the common synchronous-validator/synchronous-handler case. */
|
|
@@ -4496,24 +4750,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4496
4750
|
}
|
|
4497
4751
|
if (result instanceof Promise) {
|
|
4498
4752
|
return result.then(
|
|
4499
|
-
(settled) =>
|
|
4500
|
-
fusedRespond(
|
|
4501
|
-
settled,
|
|
4502
|
-
ctx,
|
|
4503
|
-
this.responseBodyTag,
|
|
4504
|
-
this.staticResponseHeaders,
|
|
4505
|
-
this.onResponseHooks.length === 0,
|
|
4506
|
-
),
|
|
4753
|
+
(settled) => this.respondFused(settled, ctx, this.onResponseHooks.length === 0),
|
|
4507
4754
|
(err) => logError(err, ctx),
|
|
4508
4755
|
)
|
|
4509
4756
|
}
|
|
4510
|
-
return
|
|
4511
|
-
result,
|
|
4512
|
-
ctx,
|
|
4513
|
-
this.responseBodyTag,
|
|
4514
|
-
this.staticResponseHeaders,
|
|
4515
|
-
this.onResponseHooks.length === 0,
|
|
4516
|
-
)
|
|
4757
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
4517
4758
|
}
|
|
4518
4759
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
4519
4760
|
const ctx = nativeContext
|
|
@@ -4572,6 +4813,25 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4572
4813
|
before: RawBeforeHandle,
|
|
4573
4814
|
querySchema: StandardSchemaV1 | undefined,
|
|
4574
4815
|
maxBodyBytes: number,
|
|
4816
|
+
): FusedWebRunner {
|
|
4817
|
+
return this.buildFusedDeriveBeforeAfter(
|
|
4818
|
+
handler,
|
|
4819
|
+
derive,
|
|
4820
|
+
before,
|
|
4821
|
+
undefined,
|
|
4822
|
+
querySchema,
|
|
4823
|
+
maxBodyBytes,
|
|
4824
|
+
)
|
|
4825
|
+
}
|
|
4826
|
+
|
|
4827
|
+
// Both hook variants share one registered chain; request contexts stay in arguments.
|
|
4828
|
+
private buildFusedDeriveBeforeAfter(
|
|
4829
|
+
handler: InternalHandler,
|
|
4830
|
+
derive: RawDerive,
|
|
4831
|
+
before: RawBeforeHandle,
|
|
4832
|
+
after: RawAfterHandle | undefined,
|
|
4833
|
+
querySchema: StandardSchemaV1 | undefined,
|
|
4834
|
+
maxBodyBytes: number,
|
|
4575
4835
|
): FusedWebRunner {
|
|
4576
4836
|
const logError = (err: unknown, ctx: RawContext): Response =>
|
|
4577
4837
|
this.bareError(
|
|
@@ -4580,139 +4840,83 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4580
4840
|
(result, set) => this.wrapWebResponse(toResponse(result, set)),
|
|
4581
4841
|
this.wrapWebResponse,
|
|
4582
4842
|
)
|
|
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,
|
|
4843
|
+
// Continuations capture route state once; each request supplies its own context.
|
|
4844
|
+
const runAfter = (result: unknown, ctx: RawContext): MaybePromise<Response> => {
|
|
4845
|
+
if (after === undefined)
|
|
4846
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
4847
|
+
let transformed: unknown
|
|
4848
|
+
try {
|
|
4849
|
+
transformed = after(result, ctx)
|
|
4850
|
+
} catch (err) {
|
|
4851
|
+
return logError(err, ctx)
|
|
4852
|
+
}
|
|
4853
|
+
if (transformed instanceof Promise) {
|
|
4854
|
+
return transformed.then(
|
|
4855
|
+
(value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0),
|
|
4856
|
+
(err) => logError(err, ctx),
|
|
4622
4857
|
)
|
|
4623
4858
|
}
|
|
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)
|
|
4859
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0)
|
|
4860
|
+
}
|
|
4861
|
+
const runHandler = (ctx: RawContext): MaybePromise<Response> => {
|
|
4862
|
+
let result: unknown
|
|
4863
|
+
try {
|
|
4864
|
+
result = handler(ctx)
|
|
4865
|
+
} catch (err) {
|
|
4866
|
+
return logError(err, ctx)
|
|
4642
4867
|
}
|
|
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()
|
|
4868
|
+
if (result instanceof Promise) {
|
|
4869
|
+
return result.then(
|
|
4870
|
+
(value) => runAfter(value, ctx),
|
|
4871
|
+
(err) => logError(err, ctx),
|
|
4872
|
+
)
|
|
4667
4873
|
}
|
|
4668
|
-
|
|
4669
|
-
|
|
4874
|
+
return runAfter(result, ctx)
|
|
4875
|
+
}
|
|
4876
|
+
const runBefore = (ctx: RawContext): MaybePromise<Response> => {
|
|
4877
|
+
let early: unknown
|
|
4670
4878
|
try {
|
|
4671
|
-
|
|
4879
|
+
early = before(ctx)
|
|
4672
4880
|
} catch (err) {
|
|
4673
4881
|
return logError(err, ctx)
|
|
4674
4882
|
}
|
|
4675
|
-
if (
|
|
4676
|
-
return
|
|
4883
|
+
if (early instanceof Promise) {
|
|
4884
|
+
return early.then(
|
|
4677
4885
|
(settled) => {
|
|
4678
|
-
if (settled
|
|
4679
|
-
|
|
4680
|
-
ctx.query = settled.value
|
|
4681
|
-
return runDerive()
|
|
4886
|
+
if (settled === undefined) return runHandler(ctx)
|
|
4887
|
+
return this.respondFused(settled, ctx)
|
|
4682
4888
|
},
|
|
4683
4889
|
(err) => logError(err, ctx),
|
|
4684
4890
|
)
|
|
4685
4891
|
}
|
|
4686
|
-
if (
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4892
|
+
if (early === undefined) return runHandler(ctx)
|
|
4893
|
+
return this.respondFused(early, ctx)
|
|
4894
|
+
}
|
|
4895
|
+
const runDerive = (ctx: RawContext): MaybePromise<Response> => {
|
|
4896
|
+
let derived: unknown
|
|
4897
|
+
try {
|
|
4898
|
+
derived = derive(ctx)
|
|
4899
|
+
} catch (err) {
|
|
4900
|
+
return logError(err, ctx)
|
|
4901
|
+
}
|
|
4902
|
+
if (derived instanceof Promise) {
|
|
4903
|
+
return derived.then(
|
|
4904
|
+
(settled) => {
|
|
4905
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
4906
|
+
return this.respondFused(settled, ctx)
|
|
4907
|
+
}
|
|
4908
|
+
Object.assign(ctx, settled as object)
|
|
4909
|
+
return runBefore(ctx)
|
|
4910
|
+
},
|
|
4911
|
+
(err) => logError(err, ctx),
|
|
4912
|
+
)
|
|
4913
|
+
}
|
|
4914
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
4915
|
+
return this.respondFused(derived, ctx)
|
|
4916
|
+
}
|
|
4917
|
+
Object.assign(ctx, derived as object)
|
|
4918
|
+
return runBefore(ctx)
|
|
4690
4919
|
}
|
|
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
4920
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
4717
4921
|
const ctx = nativeContext
|
|
4718
4922
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
@@ -4726,94 +4930,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4726
4930
|
maxBodyBytes,
|
|
4727
4931
|
this.protoPoisoning,
|
|
4728
4932
|
)
|
|
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()
|
|
4933
|
+
if (querySchema === undefined) return runDerive(ctx)
|
|
4817
4934
|
let validation: MaybePromise<StandardResult<unknown>>
|
|
4818
4935
|
try {
|
|
4819
4936
|
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]))
|
|
@@ -4826,7 +4943,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4826
4943
|
if (settled.issues !== undefined)
|
|
4827
4944
|
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
4828
4945
|
ctx.query = settled.value
|
|
4829
|
-
return runDerive()
|
|
4946
|
+
return runDerive(ctx)
|
|
4830
4947
|
},
|
|
4831
4948
|
(err) => logError(err, ctx),
|
|
4832
4949
|
)
|
|
@@ -4834,7 +4951,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4834
4951
|
if (validation.issues !== undefined)
|
|
4835
4952
|
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
4836
4953
|
ctx.query = validation.value
|
|
4837
|
-
return runDerive()
|
|
4954
|
+
return runDerive(ctx)
|
|
4838
4955
|
}
|
|
4839
4956
|
}
|
|
4840
4957
|
|
|
@@ -4881,13 +4998,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4881
4998
|
const runHandlerValidated = (): MaybePromise<Response> => {
|
|
4882
4999
|
const runAfter = (result: unknown): MaybePromise<Response> => {
|
|
4883
5000
|
if (after === undefined) {
|
|
4884
|
-
return
|
|
4885
|
-
result,
|
|
4886
|
-
ctx,
|
|
4887
|
-
this.responseBodyTag,
|
|
4888
|
-
this.staticResponseHeaders,
|
|
4889
|
-
this.onResponseHooks.length === 0,
|
|
4890
|
-
)
|
|
5001
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
4891
5002
|
}
|
|
4892
5003
|
let transformed: unknown
|
|
4893
5004
|
try {
|
|
@@ -4897,24 +5008,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4897
5008
|
}
|
|
4898
5009
|
if (transformed instanceof Promise) {
|
|
4899
5010
|
return transformed.then(
|
|
4900
|
-
(value) =>
|
|
4901
|
-
fusedRespond(
|
|
4902
|
-
value,
|
|
4903
|
-
ctx,
|
|
4904
|
-
this.responseBodyTag,
|
|
4905
|
-
this.staticResponseHeaders,
|
|
4906
|
-
this.onResponseHooks.length === 0,
|
|
4907
|
-
),
|
|
5011
|
+
(value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0),
|
|
4908
5012
|
(err) => logError(err, ctx),
|
|
4909
5013
|
)
|
|
4910
5014
|
}
|
|
4911
|
-
return
|
|
4912
|
-
transformed,
|
|
4913
|
-
ctx,
|
|
4914
|
-
this.responseBodyTag,
|
|
4915
|
-
this.staticResponseHeaders,
|
|
4916
|
-
this.onResponseHooks.length === 0,
|
|
4917
|
-
)
|
|
5015
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0)
|
|
4918
5016
|
}
|
|
4919
5017
|
const runHandler = (): MaybePromise<Response> => {
|
|
4920
5018
|
let result: unknown
|
|
@@ -4942,13 +5040,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4942
5040
|
return early.then(
|
|
4943
5041
|
(settled) => {
|
|
4944
5042
|
if (settled === undefined) return runHandler()
|
|
4945
|
-
return
|
|
5043
|
+
return this.respondFused(settled, ctx)
|
|
4946
5044
|
},
|
|
4947
5045
|
(err) => logError(err, ctx),
|
|
4948
5046
|
)
|
|
4949
5047
|
}
|
|
4950
5048
|
if (early === undefined) return runHandler()
|
|
4951
|
-
return
|
|
5049
|
+
return this.respondFused(early, ctx)
|
|
4952
5050
|
}
|
|
4953
5051
|
let derived: unknown
|
|
4954
5052
|
try {
|
|
@@ -4960,7 +5058,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4960
5058
|
return derived.then(
|
|
4961
5059
|
(settled) => {
|
|
4962
5060
|
if (isResponseResult(settled) || settled instanceof Response) {
|
|
4963
|
-
return
|
|
5061
|
+
return this.respondFused(settled, ctx)
|
|
4964
5062
|
}
|
|
4965
5063
|
Object.assign(ctx, settled as object)
|
|
4966
5064
|
return runBefore()
|
|
@@ -4969,7 +5067,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4969
5067
|
)
|
|
4970
5068
|
}
|
|
4971
5069
|
if (isResponseResult(derived) || derived instanceof Response) {
|
|
4972
|
-
return
|
|
5070
|
+
return this.respondFused(derived, ctx)
|
|
4973
5071
|
}
|
|
4974
5072
|
Object.assign(ctx, derived as object)
|
|
4975
5073
|
return runBefore()
|
|
@@ -5472,7 +5570,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5472
5570
|
entry: RouteEntry,
|
|
5473
5571
|
ctx: RawContext,
|
|
5474
5572
|
run: () => MaybePromise<T>,
|
|
5475
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5573
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5476
5574
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5477
5575
|
): MaybePromise<T> {
|
|
5478
5576
|
let outcome: MaybePromise<T>
|
|
@@ -5492,7 +5590,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5492
5590
|
entry: RouteEntry,
|
|
5493
5591
|
source: RequestSource,
|
|
5494
5592
|
ctx: RawContext,
|
|
5495
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5593
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5496
5594
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5497
5595
|
): MaybePromise<T> {
|
|
5498
5596
|
return executeRouteProgram(this, entry, entry.program, source, ctx, finalize, wrapResponse)
|
|
@@ -5536,7 +5634,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5536
5634
|
entry: RouteEntry,
|
|
5537
5635
|
error: unknown,
|
|
5538
5636
|
ctx: RawContext,
|
|
5539
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5637
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5540
5638
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5541
5639
|
): MaybePromise<T> {
|
|
5542
5640
|
return this.handleLifecycleError(entry, error, ctx, finalize, wrapResponse)
|
|
@@ -5548,7 +5646,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5548
5646
|
entry: RouteEntry,
|
|
5549
5647
|
ctx: RawContext,
|
|
5550
5648
|
result: unknown,
|
|
5551
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5649
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5552
5650
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5553
5651
|
): MaybePromise<T> {
|
|
5554
5652
|
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result)
|
|
@@ -5577,7 +5675,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5577
5675
|
entry: RouteEntry,
|
|
5578
5676
|
source: RequestSource,
|
|
5579
5677
|
ctx: RawContext,
|
|
5580
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5678
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5581
5679
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5582
5680
|
): Promise<T> {
|
|
5583
5681
|
return readBodyFramed(
|
|
@@ -5598,7 +5696,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5598
5696
|
entry: RouteEntry,
|
|
5599
5697
|
parsed: unknown,
|
|
5600
5698
|
ctx: RawContext,
|
|
5601
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5699
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5602
5700
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5603
5701
|
): MaybePromise<T> {
|
|
5604
5702
|
try {
|
|
@@ -5633,14 +5731,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5633
5731
|
private executeHandler<T>(
|
|
5634
5732
|
entry: RouteEntry,
|
|
5635
5733
|
ctx: RawContext,
|
|
5636
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5734
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5637
5735
|
wrapResponse?: (response: Response | ResponseResult) => T,
|
|
5638
5736
|
): MaybePromise<T> {
|
|
5639
5737
|
if (entry.hasDecorations) Object.assign(ctx, entry.decorations)
|
|
5640
5738
|
const handlerOutput = entry.handler(ctx)
|
|
5641
5739
|
if (handlerOutput instanceof Promise) {
|
|
5642
5740
|
return handlerOutput.then(
|
|
5643
|
-
(value) => finalize(value, responseSet(ctx)),
|
|
5741
|
+
(value) => finalize(value, responseSet(ctx), ctx),
|
|
5644
5742
|
(err) => {
|
|
5645
5743
|
if (wrapResponse) {
|
|
5646
5744
|
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
@@ -5649,14 +5747,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5649
5747
|
},
|
|
5650
5748
|
)
|
|
5651
5749
|
}
|
|
5652
|
-
return finalize(handlerOutput, responseSet(ctx))
|
|
5750
|
+
return finalize(handlerOutput, responseSet(ctx), ctx)
|
|
5653
5751
|
}
|
|
5654
5752
|
|
|
5655
5753
|
private handleValidationErrorRecovery<T>(
|
|
5656
5754
|
entry: RouteEntry,
|
|
5657
5755
|
recovery: unknown,
|
|
5658
5756
|
ctx: RawContext,
|
|
5659
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5757
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5660
5758
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5661
5759
|
originalIssues: ReadonlyArray<StandardIssue>,
|
|
5662
5760
|
kind: "body" | "query",
|
|
@@ -5706,7 +5804,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5706
5804
|
entry: RouteEntry,
|
|
5707
5805
|
result: StandardResult<unknown>,
|
|
5708
5806
|
ctx: RawContext,
|
|
5709
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5807
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5710
5808
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5711
5809
|
): MaybePromise<T> {
|
|
5712
5810
|
if (result.issues !== undefined) {
|
|
@@ -5746,7 +5844,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5746
5844
|
private runQueryOnly<T>(
|
|
5747
5845
|
entry: RouteEntry,
|
|
5748
5846
|
ctx: RawContext,
|
|
5749
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5847
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5750
5848
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5751
5849
|
): MaybePromise<T> {
|
|
5752
5850
|
try {
|
|
@@ -5784,7 +5882,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5784
5882
|
entry: RouteEntry,
|
|
5785
5883
|
result: StandardResult<unknown>,
|
|
5786
5884
|
ctx: RawContext,
|
|
5787
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
5885
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5788
5886
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5789
5887
|
): MaybePromise<T> {
|
|
5790
5888
|
if (result.issues !== undefined) {
|
|
@@ -5829,13 +5927,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5829
5927
|
*/
|
|
5830
5928
|
private applyOnResponse(response: Response, req: Request): MaybePromise<Response> {
|
|
5831
5929
|
const hooks = this.onResponseHooks
|
|
5832
|
-
|
|
5930
|
+
// Hooks write onto the response they are handed (cookies, CORS, request ids), so every response
|
|
5931
|
+
// entering the chain - and any other one a hook hands back - is first made this request's own.
|
|
5932
|
+
let current = writableResponse(response, req)
|
|
5833
5933
|
for (let i = 0; i < hooks.length; i++) {
|
|
5834
5934
|
const next = (hooks[i] as RawOnResponse)(current, req)
|
|
5835
5935
|
if (next instanceof Promise) {
|
|
5836
|
-
|
|
5936
|
+
const handed = current
|
|
5937
|
+
return next.then((settled) => this.continueOnResponse(settled, i + 1, req, handed))
|
|
5837
5938
|
}
|
|
5838
|
-
current = next
|
|
5939
|
+
current = next === current ? next : writableResponse(next, req)
|
|
5839
5940
|
}
|
|
5840
5941
|
return current
|
|
5841
5942
|
}
|
|
@@ -5909,11 +6010,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5909
6010
|
response: Response,
|
|
5910
6011
|
nextIndex: number,
|
|
5911
6012
|
req: Request,
|
|
6013
|
+
handed: Response,
|
|
5912
6014
|
): Promise<Response> {
|
|
5913
|
-
let current = response
|
|
6015
|
+
let current = response === handed ? response : writableResponse(response, req)
|
|
5914
6016
|
for (let i = nextIndex; i < this.onResponseHooks.length; i++) {
|
|
5915
6017
|
const next = (this.onResponseHooks[i] as RawOnResponse)(current, req)
|
|
5916
|
-
|
|
6018
|
+
const settled = next instanceof Promise ? await next : next
|
|
6019
|
+
current = settled === current ? settled : writableResponse(settled, req)
|
|
5917
6020
|
}
|
|
5918
6021
|
return current
|
|
5919
6022
|
}
|
|
@@ -5945,13 +6048,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5945
6048
|
private finishLifecycleContract<T>(
|
|
5946
6049
|
entry: RouteEntry,
|
|
5947
6050
|
ctx: RawContext,
|
|
5948
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
6051
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5949
6052
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5950
6053
|
result: unknown,
|
|
5951
6054
|
): MaybePromise<T> {
|
|
5952
6055
|
try {
|
|
5953
6056
|
const contract = entry.responseContract
|
|
5954
|
-
if (contract === undefined) return finalize(result, responseSet(ctx))
|
|
6057
|
+
if (contract === undefined) return finalize(result, responseSet(ctx), ctx)
|
|
5955
6058
|
const checked = contract.runtime.check(contract.definition, result, ctx)
|
|
5956
6059
|
if (checked instanceof Promise) {
|
|
5957
6060
|
return checked.then(
|
|
@@ -5967,7 +6070,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5967
6070
|
|
|
5968
6071
|
private finishContractOutcome<T>(
|
|
5969
6072
|
ctx: RawContext,
|
|
5970
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
6073
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5971
6074
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5972
6075
|
outcome: {
|
|
5973
6076
|
readonly kind: "ok" | "warn" | "violation"
|
|
@@ -5990,7 +6093,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5990
6093
|
detail: outcome.message,
|
|
5991
6094
|
})
|
|
5992
6095
|
}
|
|
5993
|
-
return finalize(outcome.value, responseSet(ctx))
|
|
6096
|
+
return finalize(outcome.value, responseSet(ctx), ctx)
|
|
5994
6097
|
}
|
|
5995
6098
|
|
|
5996
6099
|
/** Synchronous by design, and `MaybePromise` rather than `Promise` for the same reason: control flow
|
|
@@ -6002,7 +6105,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6002
6105
|
entry: RouteEntry,
|
|
6003
6106
|
err: unknown,
|
|
6004
6107
|
ctx: RawContext,
|
|
6005
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
6108
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
6006
6109
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6007
6110
|
): MaybePromise<T> {
|
|
6008
6111
|
// A *thrown* Response is deliberate control flow, not an error - a guard throws a redirect/401,
|
|
@@ -6010,10 +6113,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6010
6113
|
// SvelteKit semantics): as built, plus the cookies the request queued, so a session set or
|
|
6011
6114
|
// cleared before `throw redirect(...)` / `requireSession(...)` ships. Don't run onError or log
|
|
6012
6115
|
// it as a 500.
|
|
6013
|
-
if (err instanceof Response
|
|
6014
|
-
|
|
6015
|
-
//
|
|
6016
|
-
if (isResponseResult(err))
|
|
6116
|
+
if (err instanceof Response && err !== ctx.jsonReply)
|
|
6117
|
+
return finalize(err, responseSet(ctx), ctx)
|
|
6118
|
+
// Same rule for a thrown `status(...)` or `c.json(...)`, which stay held to the route's contract.
|
|
6119
|
+
if (err instanceof Response || isResponseResult(err))
|
|
6017
6120
|
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, err)
|
|
6018
6121
|
if (entry.onError.length === 0) {
|
|
6019
6122
|
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
@@ -6030,7 +6133,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6030
6133
|
entry: RouteEntry,
|
|
6031
6134
|
err: unknown,
|
|
6032
6135
|
ctx: RawContext,
|
|
6033
|
-
finalize: (result: unknown, set: CtxSet) => T,
|
|
6136
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
6034
6137
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6035
6138
|
): Promise<T> {
|
|
6036
6139
|
// Indexed for the same reason as the `beforeHandle` chain above: the iterator would outlive
|
|
@@ -6038,7 +6141,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6038
6141
|
for (let i = 0; i < entry.onError.length; i++) {
|
|
6039
6142
|
const outcome = entry.onError[i]!(err, ctx)
|
|
6040
6143
|
const handled = outcome instanceof Promise ? await outcome : outcome
|
|
6041
|
-
if (handled
|
|
6144
|
+
if (handled === undefined) continue
|
|
6145
|
+
const contract = entry.responseContract
|
|
6146
|
+
if (contract === undefined || (handled !== ctx.jsonReply && !isResponseResult(handled)))
|
|
6147
|
+
return finalize(handled, responseSet(ctx), ctx)
|
|
6148
|
+
// Checked here, not through finishLifecycleContract: a throwing schema must not re-enter onError.
|
|
6149
|
+
try {
|
|
6150
|
+
const checked = await contract.runtime.check(contract.definition, handled, ctx)
|
|
6151
|
+
return this.finishContractOutcome(ctx, finalize, wrapResponse, checked)
|
|
6152
|
+
} catch (checkError) {
|
|
6153
|
+
this.logRequestError(checkError, ctx)
|
|
6154
|
+
return wrapResponse(plainError(500, "internal_error"))
|
|
6155
|
+
}
|
|
6042
6156
|
}
|
|
6043
6157
|
this.logRequestError(err, ctx)
|
|
6044
6158
|
return wrapResponse(plainError(500, "internal_error"))
|
|
@@ -6166,7 +6280,22 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6166
6280
|
request: Request,
|
|
6167
6281
|
params: Record<string, string>,
|
|
6168
6282
|
): MaybePromise<Response> => {
|
|
6169
|
-
|
|
6283
|
+
let outcome: MaybePromise<Response>
|
|
6284
|
+
try {
|
|
6285
|
+
outcome = capped!(request, params, undefined, signal!, budget!, peer, true)
|
|
6286
|
+
} catch (err) {
|
|
6287
|
+
// A synchronous render throw, as in runMatchedLanes.
|
|
6288
|
+
outcome = this.contextlessBareError(
|
|
6289
|
+
err,
|
|
6290
|
+
request,
|
|
6291
|
+
params,
|
|
6292
|
+
undefined,
|
|
6293
|
+
signal!,
|
|
6294
|
+
budget!,
|
|
6295
|
+
peer,
|
|
6296
|
+
this.wrapWebResponse,
|
|
6297
|
+
)
|
|
6298
|
+
}
|
|
6170
6299
|
return outcome instanceof Promise
|
|
6171
6300
|
? outcome.then((response) => finishNative(request, response))
|
|
6172
6301
|
: finishNative(request, outcome)
|
|
@@ -6183,7 +6312,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6183
6312
|
markFramed(request)
|
|
6184
6313
|
return request.headers.get(NIFRA_DEADLINE_HEADER) !== null
|
|
6185
6314
|
? this.fetchMatched(request, entry, EMPTY_PARAMS, peer)
|
|
6186
|
-
:
|
|
6315
|
+
: runNative(request, EMPTY_PARAMS)
|
|
6187
6316
|
}
|
|
6188
6317
|
}
|
|
6189
6318
|
return (request) => {
|
|
@@ -6211,7 +6340,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6211
6340
|
if (malformed(params)) return fallback(request, server)
|
|
6212
6341
|
return request.headers.get(NIFRA_DEADLINE_HEADER) !== null
|
|
6213
6342
|
? this.fetchMatched(request, entry, params, peer)
|
|
6214
|
-
:
|
|
6343
|
+
: runNative(request, params)
|
|
6215
6344
|
}
|
|
6216
6345
|
}
|
|
6217
6346
|
return (request, server) => {
|
|
@@ -6239,7 +6368,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6239
6368
|
// resolves) instead of Bun's native table. The allocation-free default keeps native fusion.
|
|
6240
6369
|
if (
|
|
6241
6370
|
(this.onRequestHooks.length > 0 && !this.bunNativeRequestHooksSafe) ||
|
|
6242
|
-
this.
|
|
6371
|
+
this.wsRoutes.length > 0 ||
|
|
6243
6372
|
this.preRouteMountCount > 0 ||
|
|
6244
6373
|
this.clientIpTrust !== undefined
|
|
6245
6374
|
) {
|
|
@@ -6396,7 +6525,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6396
6525
|
}
|
|
6397
6526
|
// Seal before Bun.serve: in `"error"` mode this throws instead of leaving a bound port behind.
|
|
6398
6527
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
6399
|
-
sealHookAudit(this, this.catalog.size, this.logger)
|
|
6528
|
+
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger)
|
|
6400
6529
|
// Bun's `Server` is the concrete handle; we expose the stable `RunningServer`
|
|
6401
6530
|
// subset so the public types don't depend on the ambient `Bun` global. The cast
|
|
6402
6531
|
// bridges them - Bun's `.port` is `number | undefined` (undefined only for unix
|
|
@@ -6407,7 +6536,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6407
6536
|
// `websocket` handlers are one shared dispatcher - each connection's `ws.data.handler` is the
|
|
6408
6537
|
// matched route's handler, set by `server.upgrade`.
|
|
6409
6538
|
// With WS routes, the dispatcher comes from the installed `.use(websocket())` runtime - non-null
|
|
6410
|
-
// because
|
|
6539
|
+
// because a socket route means ws() ran, and ws() requires the runtime at registration.
|
|
6411
6540
|
// Native pub/sub when the app has WS routes and none validate outbound frames: `ws.subscribe` and
|
|
6412
6541
|
// `app.publish` go through Bun's own (uWebSockets) broadcast instead of the JS registry loop.
|
|
6413
6542
|
// A mounted app counts only while it has a WebSocket route (see `[GET_WS_RUNTIME]`), so composing
|
|
@@ -6424,7 +6553,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6424
6553
|
// that registry in the upgrade outcome instead of accidentally broadcasting child sockets through
|
|
6425
6554
|
// the parent.
|
|
6426
6555
|
const nativePubsub =
|
|
6427
|
-
this.
|
|
6556
|
+
this.wsRoutes.length > 0 && !this.mountsTakeUpgrades() && !this.wsHasValidatedSend
|
|
6428
6557
|
const wsHandlers = wsRuntime?.bunHandlers(this.topics ?? wsRuntime.createTopics(), nativePubsub)
|
|
6429
6558
|
// Spread rather than pass `hostname: undefined` (or an undefined idle timeout or TLS) - Bun treats
|
|
6430
6559
|
// an explicit undefined as a value on some option paths, and omitting is what selects its own
|
|
@@ -6492,7 +6621,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6492
6621
|
}
|
|
6493
6622
|
}
|
|
6494
6623
|
this.sealed = true
|
|
6495
|
-
if (this.gracefulSignals) this.installSignalHandlers()
|
|
6624
|
+
if (this.gracefulSignals) this.installSignalHandlers(this.gracefulSignals)
|
|
6496
6625
|
return running
|
|
6497
6626
|
}
|
|
6498
6627
|
|
|
@@ -6525,12 +6654,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6525
6654
|
])
|
|
6526
6655
|
}
|
|
6527
6656
|
|
|
6528
|
-
private installSignalHandlers(): void {
|
|
6657
|
+
private installSignalHandlers(drain: { readonly drainMs?: number }): void {
|
|
6529
6658
|
// Drain, then let the process exit naturally - the stopped server no longer
|
|
6530
6659
|
// holds the event loop open. Opt-in (`gracefulSignals`), so taking over the
|
|
6531
6660
|
// signals is consented; we don't force `process.exit`.
|
|
6532
6661
|
const onSignal = (): void => {
|
|
6533
|
-
void this.stop()
|
|
6662
|
+
void this.stop(drain)
|
|
6534
6663
|
}
|
|
6535
6664
|
process.once("SIGTERM", onSignal)
|
|
6536
6665
|
process.once("SIGINT", onSignal)
|