@nifrajs/core 5.7.0 → 5.9.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/CHANGELOG.md +105 -0
- package/README.md +9 -4
- package/dist/assurance.d.ts +26 -1
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +43 -11
- package/dist/assurance.js.map +1 -1
- package/dist/canonical-json.d.ts +11 -0
- package/dist/canonical-json.d.ts.map +1 -0
- package/dist/canonical-json.js +46 -0
- package/dist/canonical-json.js.map +1 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +1 -8
- package/dist/capabilities.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +1 -8
- package/dist/data.js.map +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +2 -12
- package/dist/diff.js.map +1 -1
- package/dist/durable-adapters.d.ts +2 -0
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +41 -49
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-saga.d.ts.map +1 -1
- package/dist/durable-saga.js +14 -20
- package/dist/durable-saga.js.map +1 -1
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +14 -68
- package/dist/evidence.js.map +1 -1
- package/dist/idempotency.d.ts.map +1 -1
- package/dist/idempotency.js +60 -23
- package/dist/idempotency.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/control-character.d.ts +3 -0
- package/dist/internal/control-character.d.ts.map +1 -0
- package/dist/internal/control-character.js +10 -0
- package/dist/internal/control-character.js.map +1 -0
- package/dist/internal/durable-shared.d.ts.map +1 -1
- package/dist/internal/durable-shared.js +4 -1
- package/dist/internal/durable-shared.js.map +1 -1
- package/dist/internal/hex.d.ts +5 -0
- package/dist/internal/hex.d.ts.map +1 -0
- package/dist/internal/hex.js +14 -0
- package/dist/internal/hex.js.map +1 -0
- package/dist/internal/record.d.ts +3 -0
- package/dist/internal/record.d.ts.map +1 -0
- package/dist/internal/record.js +5 -0
- package/dist/internal/record.js.map +1 -0
- package/dist/internal/route-compiler.d.ts.map +1 -1
- package/dist/internal/route-compiler.js +2 -2
- package/dist/internal/route-compiler.js.map +1 -1
- package/dist/internal/route-execution.d.ts +4 -4
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +4 -0
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +4 -3
- package/dist/internal/route-lanes.d.ts.map +1 -1
- package/dist/internal/route-lanes.js +16 -33
- package/dist/internal/route-lanes.js.map +1 -1
- package/dist/internal/schema-parts.d.ts +13 -0
- package/dist/internal/schema-parts.d.ts.map +1 -0
- package/dist/internal/schema-parts.js +15 -0
- package/dist/internal/schema-parts.js.map +1 -0
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +2 -14
- package/dist/ledger.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +10 -37
- package/dist/manifest.js.map +1 -1
- package/dist/open-resolved.d.ts +18 -0
- package/dist/open-resolved.d.ts.map +1 -0
- package/dist/open-resolved.js +62 -0
- package/dist/open-resolved.js.map +1 -0
- package/dist/range.js +1 -1
- package/dist/range.js.map +1 -1
- package/dist/serve-static.d.ts +22 -0
- package/dist/serve-static.d.ts.map +1 -0
- package/dist/serve-static.js +187 -0
- package/dist/serve-static.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +2 -1
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +12 -3
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/base64-answers.d.ts.map +1 -1
- package/dist/server/base64-answers.js +1 -4
- package/dist/server/base64-answers.js.map +1 -1
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +43 -38
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +4 -1
- package/dist/server/body.js.map +1 -1
- package/dist/server/bridge-markers.d.ts +1 -1
- package/dist/server/bridge-markers.js +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +4 -1
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/http.d.ts +5 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +13 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +2 -0
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/idle-timeout.d.ts +39 -0
- package/dist/server/idle-timeout.d.ts.map +1 -0
- package/dist/server/idle-timeout.js +95 -0
- package/dist/server/idle-timeout.js.map +1 -0
- package/dist/server/json-shared.d.ts +23 -0
- package/dist/server/json-shared.d.ts.map +1 -0
- package/dist/server/json-shared.js +33 -0
- package/dist/server/json-shared.js.map +1 -0
- package/dist/server/json-text.d.ts +1 -1
- package/dist/server/json-text.js +1 -1
- package/dist/server/node-outcome.js +1 -1
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/request-context.d.ts +22 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +44 -4
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/request-local.d.ts +44 -0
- package/dist/server/request-local.d.ts.map +1 -0
- package/dist/server/request-local.js +91 -0
- package/dist/server/request-local.js.map +1 -0
- package/dist/server/respond.js +1 -1
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +10 -2
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +93 -64
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/runtime-core.d.ts +7 -4
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +9 -7
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +27 -0
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +59 -59
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +869 -512
- package/dist/server/server.js.map +1 -1
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js +2 -0
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/webhook.js +1 -1
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +3 -1
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +14 -1
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-adapter-view.d.ts +54 -0
- package/dist/server/ws-adapter-view.d.ts.map +1 -0
- package/dist/server/ws-adapter-view.js +7 -0
- package/dist/server/ws-adapter-view.js.map +1 -0
- package/dist/server.d.ts +2 -1
- 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.map +1 -1
- package/dist/single-copy.js +1 -1
- package/dist/single-copy.js.map +1 -1
- package/dist/static-file.d.ts +156 -4
- package/dist/static-file.d.ts.map +1 -1
- package/dist/static-file.js +198 -4
- package/dist/static-file.js.map +1 -1
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +1 -3
- package/dist/wire.js.map +1 -1
- package/package.json +16 -1
- package/src/assurance.ts +66 -16
- package/src/canonical-json.ts +43 -0
- package/src/capabilities.ts +1 -8
- package/src/data.ts +2 -8
- package/src/diff.ts +2 -12
- package/src/durable-adapters.ts +53 -50
- package/src/durable-saga.ts +30 -23
- package/src/evidence.ts +25 -90
- package/src/idempotency.ts +62 -20
- package/src/index.ts +13 -2
- package/src/internal/control-character.ts +8 -0
- package/src/internal/durable-shared.ts +4 -1
- package/src/internal/hex.ts +14 -0
- package/src/internal/record.ts +4 -0
- package/src/internal/route-compiler.ts +2 -2
- package/src/internal/route-execution.ts +7 -20
- package/src/internal/route-lanes.ts +20 -44
- package/src/internal/schema-parts.ts +19 -0
- package/src/ledger.ts +2 -16
- package/src/manifest.ts +10 -38
- package/src/open-resolved.ts +64 -0
- package/src/range.ts +1 -1
- package/src/serve-static.ts +227 -0
- package/src/server/bare-error-lane.ts +11 -2
- package/src/server/base64-answers.ts +2 -6
- package/src/server/body-lane.ts +58 -51
- package/src/server/body.ts +4 -1
- package/src/server/bridge-markers.ts +1 -1
- package/src/server/cookies.ts +4 -1
- package/src/server/core-copies.ts +1 -1
- package/src/server/http.ts +12 -0
- package/src/server/idempotency-lane.ts +2 -0
- package/src/server/idle-timeout.ts +107 -0
- package/src/server/json-shared.ts +38 -0
- package/src/server/json-text.ts +1 -1
- package/src/server/node-outcome.ts +1 -1
- package/src/server/request-context.ts +62 -3
- package/src/server/request-local.ts +109 -0
- package/src/server/respond.ts +1 -1
- package/src/server/response-contract-lane.ts +112 -65
- package/src/server/runtime-core.ts +10 -8
- package/src/server/server-types.ts +27 -0
- package/src/server/server.ts +1230 -800
- package/src/server/transport-lane.ts +2 -0
- package/src/server/webhook.ts +1 -1
- package/src/server/websocket.ts +18 -1
- package/src/server/ws-adapter-view.ts +63 -0
- package/src/server.ts +12 -1
- package/src/single-copy.ts +1 -3
- package/src/static-file.ts +350 -4
- package/src/wire.ts +2 -4
package/src/server/server.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
import { isSameOriginRequest } from "../internal/same-origin.ts"
|
|
59
59
|
import type { ResponseObserverPlugin } from "../response-observer.ts"
|
|
60
60
|
import {
|
|
61
|
+
compileRoutePattern,
|
|
61
62
|
decodeRouteParams,
|
|
62
63
|
expandOptionalParams,
|
|
63
64
|
type RoutePatternSegment,
|
|
@@ -124,11 +125,14 @@ import type {
|
|
|
124
125
|
import { type QueryValue, queryObjectOf, searchOf } from "./query.ts"
|
|
125
126
|
import {
|
|
126
127
|
type PeerPlatform,
|
|
128
|
+
PLATFORM_IDLE_TIMEOUT,
|
|
127
129
|
PLATFORM_PEER,
|
|
130
|
+
PLATFORM_REQUEST,
|
|
128
131
|
RequestContext,
|
|
129
132
|
readBodyFramed,
|
|
130
133
|
} from "./request-context.ts"
|
|
131
134
|
import { recordRequestReplacement } from "./request-lineage.ts"
|
|
135
|
+
import { carryToCopy } from "./request-local.ts"
|
|
132
136
|
import { needsResolving, resolveDotSegments } from "./request-target.ts"
|
|
133
137
|
import {
|
|
134
138
|
applyStaticResponseHeaders,
|
|
@@ -347,10 +351,11 @@ interface WsEntry {
|
|
|
347
351
|
readonly chain: RouteEntry
|
|
348
352
|
}
|
|
349
353
|
|
|
350
|
-
/** Structural view of the socket-peer lookup on the Bun `Server`, so
|
|
351
|
-
* satisfies it. */
|
|
354
|
+
/** Structural view of the socket-peer lookup and per-request idle timeout on the Bun `Server`, so
|
|
355
|
+
* any Bun `Server` (WS or not) satisfies it. */
|
|
352
356
|
interface BunPeerServer {
|
|
353
357
|
requestIP(request: Request): { readonly address: string } | null
|
|
358
|
+
timeout(request: Request, seconds: number): void
|
|
354
359
|
}
|
|
355
360
|
|
|
356
361
|
/** Structural view of the Bun `Server` the `fetch` 2nd arg exposes (`upgrade` + the socket peer). */
|
|
@@ -405,23 +410,44 @@ interface FetchMount<Env = unknown> {
|
|
|
405
410
|
wsPassed?: WeakMap<Request, Request>
|
|
406
411
|
}
|
|
407
412
|
|
|
408
|
-
/**
|
|
409
|
-
|
|
413
|
+
/** A route Bun's table matched, as the dispatcher would have. */
|
|
414
|
+
interface MatchedRoute {
|
|
415
|
+
readonly entry: RouteEntry
|
|
416
|
+
readonly params: Record<string, string>
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
type IdleTimeoutSetter = NonNullable<PeerPlatform[typeof PLATFORM_IDLE_TIMEOUT]>
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The socket peer Bun observed (`undefined` if unknown) and its connection's idle timeout, as a
|
|
423
|
+
* `Platform` for one request. A class, not an object literal: JSC builds a literal holding a getter
|
|
424
|
+
* or a symbol key on a slow path, measured at about 5x the instructions of this instance.
|
|
425
|
+
*/
|
|
426
|
+
class BunPeerPlatform implements PeerPlatform {
|
|
427
|
+
readonly #server: BunPeerServer
|
|
428
|
+
#resolved = false
|
|
429
|
+
#address: string | undefined
|
|
430
|
+
// Own fields, so the copy a `clientIp` trust declaration makes (`{ ...platform }`) keeps them.
|
|
431
|
+
declare readonly [PLATFORM_IDLE_TIMEOUT]: IdleTimeoutSetter
|
|
432
|
+
declare readonly [PLATFORM_REQUEST]: Request
|
|
433
|
+
|
|
434
|
+
constructor(server: BunPeerServer, req: Request, setIdleTimeout: IdleTimeoutSetter) {
|
|
435
|
+
this.#server = server
|
|
436
|
+
this[PLATFORM_IDLE_TIMEOUT] = setIdleTimeout
|
|
437
|
+
this[PLATFORM_REQUEST] = req
|
|
438
|
+
}
|
|
439
|
+
|
|
410
440
|
// Bun's requestIP() costs ~0.5 us per call, about 7% of a bare GET's server time. Keep the
|
|
411
441
|
// documented raw-peer c.clientIp behavior, but resolve it lazily: most routes never read c.clientIp,
|
|
412
442
|
// so they should not pay for the socket lookup on every request. A getter also
|
|
413
443
|
// preserves middleware that inspects the platform argument directly and trust-mode routes, which
|
|
414
444
|
// resolve the value in deriveClientIp before the handler runs.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
address = server.requestIP(req)?.address
|
|
422
|
-
}
|
|
423
|
-
return address
|
|
424
|
-
},
|
|
445
|
+
get clientIp(): string | undefined {
|
|
446
|
+
if (!this.#resolved) {
|
|
447
|
+
this.#resolved = true
|
|
448
|
+
this.#address = this.#server.requestIP(this[PLATFORM_REQUEST])?.address
|
|
449
|
+
}
|
|
450
|
+
return this.#address
|
|
425
451
|
}
|
|
426
452
|
}
|
|
427
453
|
|
|
@@ -458,6 +484,23 @@ function outranks(
|
|
|
458
484
|
return ahead && other.length === route.length
|
|
459
485
|
}
|
|
460
486
|
|
|
487
|
+
/** Whether one request can match both patterns: no two static segments differ before either ends
|
|
488
|
+
* in a wildcard, and the lengths agree otherwise. Errs toward `true`. */
|
|
489
|
+
function mayShareRequest(
|
|
490
|
+
a: CatalogRoute["pattern"]["segments"],
|
|
491
|
+
b: CatalogRoute["pattern"]["segments"],
|
|
492
|
+
): boolean {
|
|
493
|
+
const shared = Math.min(a.length, b.length)
|
|
494
|
+
for (let i = 0; i < shared; i++) {
|
|
495
|
+
const left = a[i]!
|
|
496
|
+
const right = b[i]!
|
|
497
|
+
if (left.kind === "wildcard" || right.kind === "wildcard") return true
|
|
498
|
+
if (left.kind === "static" && right.kind === "static" && left.value !== right.value)
|
|
499
|
+
return false
|
|
500
|
+
}
|
|
501
|
+
return a.length === b.length
|
|
502
|
+
}
|
|
503
|
+
|
|
461
504
|
/**
|
|
462
505
|
* The key Bun's route table holds `path` under. Bun refuses one with a character outside printable
|
|
463
506
|
* ASCII, so such a path is spelled as the portable router matches it: percent-encoded static
|
|
@@ -611,6 +654,8 @@ const BODY_ERROR = (error: unknown): Promise<never> => Promise.reject(error)
|
|
|
611
654
|
const DEFAULT_DRAIN_MS = 10_000
|
|
612
655
|
const DRAIN_POLL_MS = 10
|
|
613
656
|
const STOP_HOOK_TIMEOUT_MS = 5_000
|
|
657
|
+
/** Bun's own `idleTimeout` when `listen()` is given none. */
|
|
658
|
+
const BUN_IDLE_TIMEOUT_SEC = 10
|
|
614
659
|
|
|
615
660
|
/** Same-origin check for a WebSocket handshake (CSWSH default). {@link isSameOriginRequest} is the one
|
|
616
661
|
* owner, shared with the server-function mount in `@nifrajs/web` - the two used to answer differently
|
|
@@ -652,8 +697,8 @@ function hasUnroutedParam(params: Record<string, string>): boolean {
|
|
|
652
697
|
}
|
|
653
698
|
|
|
654
699
|
/**
|
|
655
|
-
* Deno
|
|
656
|
-
* Bun, workerd and
|
|
700
|
+
* Deno's server builds `Request.url` from the Host and target as received, without normalizing the
|
|
701
|
+
* target first. Bun, workerd, and `new Request()` parse it before core sees it.
|
|
657
702
|
*/
|
|
658
703
|
const rawRequestTargets = typeof (globalThis as { Deno?: unknown }).Deno !== "undefined"
|
|
659
704
|
|
|
@@ -679,6 +724,7 @@ function withResolvedTarget(req: Request, url: string): Request | undefined {
|
|
|
679
724
|
}
|
|
680
725
|
// The same body, so the same framing: a runtime still delimited it.
|
|
681
726
|
if (hasTrustedBodyFraming(req)) markTrustedBodyFraming(rebuilt)
|
|
727
|
+
carryToCopy(req, rebuilt)
|
|
682
728
|
return rebuilt
|
|
683
729
|
}
|
|
684
730
|
|
|
@@ -713,7 +759,8 @@ function resolvedSource(req: Request, url: string): RequestSource {
|
|
|
713
759
|
const HOST_AUTHORITY = /^(?:\[[0-9a-f:.%]+\]|[\w.-]+)(?::(\d{1,5}))?$/i
|
|
714
760
|
|
|
715
761
|
// Clients repeat one Host on every request; comparing to the last valid one is ~7x cheaper than the regex.
|
|
716
|
-
|
|
762
|
+
// `null` until one passes, so an empty Host is never taken as the cached one.
|
|
763
|
+
let lastValidHost: string | null = null
|
|
717
764
|
|
|
718
765
|
// A port past 65535, or an IPv6 literal no URL parser takes (`[1:2]`), is refused as Node refuses it:
|
|
719
766
|
// no URL can be built on either, so resolving a dot segment against one threw out of `fetch`.
|
|
@@ -732,23 +779,35 @@ const invalidHost = (host: string): boolean => {
|
|
|
732
779
|
* Host (400), a URL replaces the target, `false` keeps it. Deno hands the target over as sent, so
|
|
733
780
|
* its dot segments are resolved; so does Bun when HTTP/1.0 leaves the Host out, and its route table
|
|
734
781
|
* matched `/files/..` raw. Such a request is served as `localhost`, as Node and Deno serve it.
|
|
735
|
-
* `headerOf`, not `req.headers`: on Deno
|
|
736
|
-
*
|
|
782
|
+
* `headerOf`, not `req.headers`: on Deno it reads Host only when `validateHost` is set, and dot
|
|
783
|
+
* segments are resolved only when `normalizeTarget` is. On Bun only a target that needs resolving
|
|
784
|
+
* is checked unless `validateHost` is set.
|
|
737
785
|
*/
|
|
738
|
-
const requestTarget = (
|
|
739
|
-
|
|
786
|
+
const requestTarget = (
|
|
787
|
+
req: Request,
|
|
788
|
+
validateHost: boolean,
|
|
789
|
+
normalizeTarget: boolean,
|
|
790
|
+
): string | boolean => {
|
|
791
|
+
if (rawRequestTargets) {
|
|
792
|
+
const host = validateHost ? headerOf(req, "host") : null
|
|
793
|
+
if (host !== null && invalidHost(host)) return true
|
|
794
|
+
if (!normalizeTarget || !needsResolving(req.url)) return false
|
|
795
|
+
// Deno can preserve an unparseable absolute URL when Host is malformed. Do not route its
|
|
796
|
+
// unresolved dot segments through the raw-target fallback.
|
|
797
|
+
if (!URL.canParse(req.url, "http://localhost")) return true
|
|
798
|
+
return resolveDotSegments(req.url)
|
|
799
|
+
}
|
|
800
|
+
if (!(unparsedBunTargets && (validateHost || needsResolving(req.url)))) return false
|
|
740
801
|
const host = headerOf(req, "host")
|
|
741
802
|
if (host !== null && invalidHost(host)) {
|
|
742
|
-
if (rawRequestTargets) return true
|
|
743
803
|
// Bun served it anyway: it could not read the Host, so it left the target unparsed, or it
|
|
744
804
|
// built the URL on it - sent twice (joined with ", ", the URL on the first; RFC 9112 section
|
|
745
|
-
// 3.2 answers 400), percent-encoded (
|
|
746
|
-
// sub-delimiter (
|
|
805
|
+
// 3.2 answers 400), percent-encoded (%61.example routed as a.example), or with a
|
|
806
|
+
// sub-delimiter (a!b). A request built in process keeps the URL its caller built, unless Bun
|
|
747
807
|
// would have built it so.
|
|
748
808
|
const routed = URL.parse(req.url)?.host
|
|
749
809
|
return routed === undefined || URL.parse(`//${host.split(", ")[0]}`, req.url)?.host === routed
|
|
750
810
|
}
|
|
751
|
-
if (rawRequestTargets) return needsResolving(req.url) && resolveDotSegments(req.url)
|
|
752
811
|
// A request built in process carries an absolute URL whatever its headers say.
|
|
753
812
|
return (
|
|
754
813
|
host === null &&
|
|
@@ -787,7 +846,8 @@ function compareMounts<Env>(a: FetchMount<Env>, b: FetchMount<Env>): number {
|
|
|
787
846
|
return b.priority - a.priority || b.path.length - a.path.length || a.order - b.order
|
|
788
847
|
}
|
|
789
848
|
|
|
790
|
-
/** Rebase a mounted request's path without losing the authentication mark used by child caches
|
|
849
|
+
/** Rebase a mounted request's path without losing the authentication mark used by child caches, or
|
|
850
|
+
* its request locals. */
|
|
791
851
|
function stripMountPrefix(request: Request, prefix: string): Request {
|
|
792
852
|
if (prefix === "/") return request
|
|
793
853
|
const url = new URL(request.url)
|
|
@@ -795,6 +855,7 @@ function stripMountPrefix(request: Request, prefix: string): Request {
|
|
|
795
855
|
url.pathname = rest === "" ? "/" : rest
|
|
796
856
|
const childRequest = new Request(url.href, request)
|
|
797
857
|
if (isAuthenticatedRequest(request)) markAuthenticatedRequest(childRequest)
|
|
858
|
+
carryToCopy(request, childRequest)
|
|
798
859
|
return childRequest
|
|
799
860
|
}
|
|
800
861
|
|
|
@@ -810,6 +871,74 @@ function responseSet(ctx: RawContext): CtxSet {
|
|
|
810
871
|
return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS
|
|
811
872
|
}
|
|
812
873
|
|
|
874
|
+
type Finalize<T> = (result: unknown, set: CtxSet, ctx: RawContext) => T
|
|
875
|
+
type Wrap<T> = (response: Response | ResponseResult) => T
|
|
876
|
+
|
|
877
|
+
/** A fused route's handler, hook arrays, error hooks and decorations, which never change after the
|
|
878
|
+
* route is built. The route's shared stage chain reads them per request, so routes whose hooks differ
|
|
879
|
+
* still share one chain. */
|
|
880
|
+
interface FusedHooks {
|
|
881
|
+
readonly handler: InternalHandler
|
|
882
|
+
readonly derives: readonly RawDerive[]
|
|
883
|
+
readonly befores: readonly RawBeforeHandle[]
|
|
884
|
+
readonly afters: readonly RawAfterHandle[]
|
|
885
|
+
readonly errors: readonly RawErrorHandler[]
|
|
886
|
+
readonly decorations: Record<PropertyKey, unknown> | undefined
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** What a fused lifecycle route runs for its hook shape on the Web lane: the first derive, or the
|
|
890
|
+
* handler when the shape has no hooks before it. */
|
|
891
|
+
type FusedWebStart = (ctx: RawContext, route: FusedHooks, index: number) => MaybePromise<Response>
|
|
892
|
+
|
|
893
|
+
/** The same chain for the Node-direct lane, finalizing through the caller's `finalize`. */
|
|
894
|
+
type FusedNodeStart = <T>(
|
|
895
|
+
ctx: RawContext,
|
|
896
|
+
route: FusedHooks,
|
|
897
|
+
finalize: Finalize<T>,
|
|
898
|
+
wrapResponse: Wrap<T>,
|
|
899
|
+
index: number,
|
|
900
|
+
) => MaybePromise<T>
|
|
901
|
+
|
|
902
|
+
/** The error hooks of the many fused routes that have none, shared so each does not allocate its own. */
|
|
903
|
+
const NO_ERROR_HOOKS: readonly RawErrorHandler[] = []
|
|
904
|
+
|
|
905
|
+
const WEB_STAGES = new WeakMap<object, Map<number, FusedWebStart>>()
|
|
906
|
+
const NODE_STAGES = new WeakMap<object, Map<number, FusedNodeStart>>()
|
|
907
|
+
|
|
908
|
+
/** How many hooks a phase runs, as the chain tells them apart: none, one, or several. */
|
|
909
|
+
type HookCount = 0 | 1 | 2
|
|
910
|
+
|
|
911
|
+
const hookCount = (hooks: readonly object[]): HookCount =>
|
|
912
|
+
hooks.length > 1 ? 2 : (hooks.length as 0 | 1)
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* The fused stage chain of a hook shape, built once per server and handed to every route with the same
|
|
916
|
+
* number of derive, before and after hooks (none, one or several of each). The chain reads a route's
|
|
917
|
+
* hooks and handler from the `FusedHooks` it is called with, so sharing it changes no answer.
|
|
918
|
+
*/
|
|
919
|
+
function sharedStages<T>(
|
|
920
|
+
cache: WeakMap<object, Map<number, T>>,
|
|
921
|
+
owner: object,
|
|
922
|
+
route: FusedHooks,
|
|
923
|
+
build: (derives: HookCount, befores: HookCount, afters: HookCount) => T,
|
|
924
|
+
): T {
|
|
925
|
+
const derives = hookCount(route.derives)
|
|
926
|
+
const befores = hookCount(route.befores)
|
|
927
|
+
const afters = hookCount(route.afters)
|
|
928
|
+
const key = derives * 9 + befores * 3 + afters
|
|
929
|
+
let chains = cache.get(owner)
|
|
930
|
+
if (chains === undefined) {
|
|
931
|
+
chains = new Map()
|
|
932
|
+
cache.set(owner, chains)
|
|
933
|
+
}
|
|
934
|
+
let stages = chains.get(key)
|
|
935
|
+
if (stages === undefined) {
|
|
936
|
+
stages = build(derives, befores, afters)
|
|
937
|
+
chains.set(key, stages)
|
|
938
|
+
}
|
|
939
|
+
return stages
|
|
940
|
+
}
|
|
941
|
+
|
|
813
942
|
function isContextlessNoArgArrow(handler: (context: never) => unknown): boolean {
|
|
814
943
|
if (handler.length !== 0) return false
|
|
815
944
|
try {
|
|
@@ -1270,6 +1399,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1270
1399
|
private readonly formOrigins: FormOrigins | undefined
|
|
1271
1400
|
/** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
|
|
1272
1401
|
private readonly trustBodyFraming: boolean
|
|
1402
|
+
private readonly validateHost: boolean
|
|
1403
|
+
private readonly normalizeTarget: boolean
|
|
1273
1404
|
private readonly wsMaxPayloadBytes: number
|
|
1274
1405
|
private readonly requestTimeoutMs: number
|
|
1275
1406
|
private readonly wsUpgradeTimeoutMs: number
|
|
@@ -1414,6 +1545,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1414
1545
|
this.protoPoisoning = options.protoPoisoning ?? "reject"
|
|
1415
1546
|
this.formOrigins = options.formOrigins
|
|
1416
1547
|
this.trustBodyFraming = options.trustBodyFraming ?? false
|
|
1548
|
+
this.validateHost = options.validateHost ?? false
|
|
1549
|
+
this.normalizeTarget = options.normalizeTarget ?? false
|
|
1417
1550
|
const wsMaxPayloadBytes = options.wsMaxPayloadBytes ?? maxBodyBytes
|
|
1418
1551
|
assertByteLimit(wsMaxPayloadBytes, "wsMaxPayloadBytes")
|
|
1419
1552
|
this.maxBodyBytes = maxBodyBytes
|
|
@@ -2658,18 +2791,28 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2658
2791
|
handler = this.idempotencyRuntime.wrapHandler(handler)
|
|
2659
2792
|
}
|
|
2660
2793
|
const { bare, fusedQuery, fusedBody } = lanes
|
|
2661
|
-
//
|
|
2662
|
-
//
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2794
|
+
// `selectRouteLanes` admits the fused hook lanes only for derive, before and after hooks, so the
|
|
2795
|
+
// three arrays below are the route's whole lifecycle.
|
|
2796
|
+
const fusedLifecycle = lanes.fusedLane === "hooks" || lanes.fusedLane === "body-hooks"
|
|
2797
|
+
// The fused chains keep the arrays they are built from, and `beforeHandle()` inserts into the live
|
|
2798
|
+
// ones ahead of `beforeHandler()` guards, so the entry and the fused chains share one snapshot of
|
|
2799
|
+
// this route's.
|
|
2800
|
+
const derivesSnapshot = [...this.derives]
|
|
2801
|
+
const beforesSnapshot = [...beforeHandle]
|
|
2802
|
+
const aftersSnapshot = [...afterHandle]
|
|
2803
|
+
const onErrorSnapshot = onError.length === 0 ? NO_ERROR_HOOKS : [...onError]
|
|
2804
|
+
const fusedHooks: FusedHooks | undefined = fusedLifecycle
|
|
2805
|
+
? {
|
|
2806
|
+
handler: handler as unknown as InternalHandler,
|
|
2807
|
+
derives: derivesSnapshot,
|
|
2808
|
+
befores: beforesSnapshot,
|
|
2809
|
+
afters: aftersSnapshot,
|
|
2810
|
+
errors: onErrorSnapshot,
|
|
2811
|
+
decorations: hasDecorations ? routeDecorations : undefined,
|
|
2812
|
+
}
|
|
2813
|
+
: undefined
|
|
2814
|
+
// The Node-direct dispatcher (`!webFast`) prefers these runners over the generic program. Only the
|
|
2815
|
+
// body-only runner also feeds the Web wrapper; the lifecycle lanes build their own Web closure.
|
|
2673
2816
|
const fusedBodyRunner = fusedBody
|
|
2674
2817
|
? this.buildFusedBodyRunner(
|
|
2675
2818
|
handler as unknown as InternalHandler,
|
|
@@ -2677,34 +2820,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2677
2820
|
hasDecorations ? routeDecorations : undefined,
|
|
2678
2821
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2679
2822
|
)
|
|
2680
|
-
:
|
|
2681
|
-
? this.
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
afterHandle[0],
|
|
2686
|
-
schema?.body as StandardSchemaV1,
|
|
2823
|
+
: fusedHooks !== undefined
|
|
2824
|
+
? this.buildFusedLifecycleRunner(
|
|
2825
|
+
fusedHooks,
|
|
2826
|
+
schema?.query,
|
|
2827
|
+
schema?.body as StandardSchemaV1 | undefined,
|
|
2687
2828
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2688
2829
|
)
|
|
2689
|
-
:
|
|
2690
|
-
? this.buildFusedDeriveBeforeRunner(
|
|
2691
|
-
handler as unknown as InternalHandler,
|
|
2692
|
-
this.derives[0]!,
|
|
2693
|
-
beforeHandle[0]!,
|
|
2694
|
-
undefined,
|
|
2695
|
-
schema?.query,
|
|
2696
|
-
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2697
|
-
)
|
|
2698
|
-
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
2699
|
-
? this.buildFusedDeriveBeforeRunner(
|
|
2700
|
-
handler as unknown as InternalHandler,
|
|
2701
|
-
this.derives[0]!,
|
|
2702
|
-
beforeHandle[0]!,
|
|
2703
|
-
afterHandle[0],
|
|
2704
|
-
schema?.query,
|
|
2705
|
-
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2706
|
-
)
|
|
2707
|
-
: undefined
|
|
2830
|
+
: undefined
|
|
2708
2831
|
const fusedWeb =
|
|
2709
2832
|
bare && this.aroundHooks.length === 0
|
|
2710
2833
|
? this.buildFusedWeb(
|
|
@@ -2722,33 +2845,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2722
2845
|
)
|
|
2723
2846
|
: fusedBody
|
|
2724
2847
|
? this.buildFusedBodyWeb(fusedBodyRunner as FusedBodyRunner)
|
|
2725
|
-
:
|
|
2726
|
-
? this.
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
afterHandle[0],
|
|
2731
|
-
schema?.body as StandardSchemaV1,
|
|
2848
|
+
: fusedHooks !== undefined
|
|
2849
|
+
? this.buildFusedLifecycleWeb(
|
|
2850
|
+
fusedHooks,
|
|
2851
|
+
schema?.query,
|
|
2852
|
+
schema?.body as StandardSchemaV1 | undefined,
|
|
2732
2853
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2733
2854
|
)
|
|
2734
|
-
:
|
|
2735
|
-
? this.buildFusedDeriveBefore(
|
|
2736
|
-
handler as unknown as InternalHandler,
|
|
2737
|
-
this.derives[0]!,
|
|
2738
|
-
beforeHandle[0]!,
|
|
2739
|
-
schema?.query,
|
|
2740
|
-
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2741
|
-
)
|
|
2742
|
-
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
2743
|
-
? this.buildFusedDeriveBeforeAfter(
|
|
2744
|
-
handler as unknown as InternalHandler,
|
|
2745
|
-
this.derives[0]!,
|
|
2746
|
-
beforeHandle[0]!,
|
|
2747
|
-
afterHandle[0]!,
|
|
2748
|
-
schema?.query,
|
|
2749
|
-
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2750
|
-
)
|
|
2751
|
-
: undefined
|
|
2855
|
+
: undefined
|
|
2752
2856
|
const program = compileRouteProgram({
|
|
2753
2857
|
schema,
|
|
2754
2858
|
handler: handler as unknown as InternalHandler,
|
|
@@ -2781,13 +2885,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2781
2885
|
idempotent,
|
|
2782
2886
|
ledgered,
|
|
2783
2887
|
responseContract,
|
|
2784
|
-
derives:
|
|
2888
|
+
derives: derivesSnapshot,
|
|
2785
2889
|
authStages: [...compiled.authStages],
|
|
2786
2890
|
decorations: routeDecorations,
|
|
2787
2891
|
hasDecorations,
|
|
2788
|
-
beforeHandle:
|
|
2789
|
-
afterHandle:
|
|
2790
|
-
onError:
|
|
2892
|
+
beforeHandle: beforesSnapshot,
|
|
2893
|
+
afterHandle: aftersSnapshot,
|
|
2894
|
+
onError: onErrorSnapshot,
|
|
2791
2895
|
lifecycleHookLane: lanes.lifecycleHookLane,
|
|
2792
2896
|
around: [...this.aroundHooks],
|
|
2793
2897
|
execution,
|
|
@@ -3242,13 +3346,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3242
3346
|
const lane = entry.execution.fusedLane
|
|
3243
3347
|
// Type-erase the lane so each `===` branch sees the full union, not the narrowed remainder.
|
|
3244
3348
|
const laneName: string | undefined = lane
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3349
|
+
const lifecycle = laneName === "hooks" || laneName === "body-hooks"
|
|
3350
|
+
const hooks: FusedHooks | undefined = lifecycle
|
|
3351
|
+
? {
|
|
3352
|
+
handler: entry.handler,
|
|
3353
|
+
derives: entry.derives,
|
|
3354
|
+
befores: entry.beforeHandle,
|
|
3355
|
+
afters: entry.afterHandle,
|
|
3356
|
+
errors: entry.onError,
|
|
3357
|
+
decorations: entry.hasDecorations ? entry.decorations : undefined,
|
|
3358
|
+
}
|
|
3359
|
+
: undefined
|
|
3252
3360
|
const fusedBody =
|
|
3253
3361
|
laneName === "body"
|
|
3254
3362
|
? this.buildFusedBodyRunner(
|
|
@@ -3257,49 +3365,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3257
3365
|
entry.hasDecorations ? entry.decorations : undefined,
|
|
3258
3366
|
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3259
3367
|
)
|
|
3260
|
-
:
|
|
3261
|
-
? this.
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
undefined,
|
|
3266
|
-
entry.schema?.body as StandardSchemaV1,
|
|
3368
|
+
: hooks !== undefined
|
|
3369
|
+
? this.buildFusedLifecycleRunner(
|
|
3370
|
+
hooks,
|
|
3371
|
+
entry.schema?.query,
|
|
3372
|
+
entry.schema?.body as StandardSchemaV1 | undefined,
|
|
3267
3373
|
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3268
3374
|
)
|
|
3269
|
-
:
|
|
3270
|
-
derive !== undefined &&
|
|
3271
|
-
before !== undefined &&
|
|
3272
|
-
after !== undefined
|
|
3273
|
-
? this.buildFusedBodyDeriveBeforeAfterRunner(
|
|
3274
|
-
entry.handler,
|
|
3275
|
-
derive,
|
|
3276
|
-
before,
|
|
3277
|
-
after,
|
|
3278
|
-
entry.schema?.body as StandardSchemaV1,
|
|
3279
|
-
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3280
|
-
)
|
|
3281
|
-
: laneName === "derive-before" && derive !== undefined && before !== undefined
|
|
3282
|
-
? this.buildFusedDeriveBeforeRunner(
|
|
3283
|
-
entry.handler,
|
|
3284
|
-
derive,
|
|
3285
|
-
before,
|
|
3286
|
-
undefined,
|
|
3287
|
-
entry.schema?.query,
|
|
3288
|
-
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3289
|
-
)
|
|
3290
|
-
: laneName === "derive-before-after" &&
|
|
3291
|
-
derive !== undefined &&
|
|
3292
|
-
before !== undefined &&
|
|
3293
|
-
after !== undefined
|
|
3294
|
-
? this.buildFusedDeriveBeforeRunner(
|
|
3295
|
-
entry.handler,
|
|
3296
|
-
derive,
|
|
3297
|
-
before,
|
|
3298
|
-
after,
|
|
3299
|
-
entry.schema?.query,
|
|
3300
|
-
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3301
|
-
)
|
|
3302
|
-
: undefined
|
|
3375
|
+
: undefined
|
|
3303
3376
|
const fusedWeb =
|
|
3304
3377
|
laneName === "body"
|
|
3305
3378
|
? this.buildFusedBodyWeb(fusedBody as FusedBodyRunner)
|
|
@@ -3310,53 +3383,19 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3310
3383
|
entry.schema?.query as StandardSchemaV1,
|
|
3311
3384
|
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3312
3385
|
)
|
|
3313
|
-
:
|
|
3314
|
-
? this.
|
|
3386
|
+
: hooks !== undefined
|
|
3387
|
+
? this.buildFusedLifecycleWeb(
|
|
3388
|
+
hooks,
|
|
3389
|
+
entry.schema?.query,
|
|
3390
|
+
entry.schema?.body as StandardSchemaV1 | undefined,
|
|
3391
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3392
|
+
)
|
|
3393
|
+
: this.buildFusedWeb(
|
|
3315
3394
|
entry.handler,
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
undefined,
|
|
3319
|
-
entry.schema?.body as StandardSchemaV1,
|
|
3395
|
+
entry.hasDecorations ? entry.decorations : undefined,
|
|
3396
|
+
isContextlessNoArgArrow(entry.handler),
|
|
3320
3397
|
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3321
3398
|
)
|
|
3322
|
-
: laneName === "body-derive-before-after" &&
|
|
3323
|
-
derive !== undefined &&
|
|
3324
|
-
before !== undefined &&
|
|
3325
|
-
after !== undefined
|
|
3326
|
-
? this.buildFusedBodyDeriveBeforeAfter(
|
|
3327
|
-
entry.handler,
|
|
3328
|
-
derive,
|
|
3329
|
-
before,
|
|
3330
|
-
after,
|
|
3331
|
-
entry.schema?.body as StandardSchemaV1,
|
|
3332
|
-
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3333
|
-
)
|
|
3334
|
-
: laneName === "derive-before" && derive !== undefined && before !== undefined
|
|
3335
|
-
? this.buildFusedDeriveBefore(
|
|
3336
|
-
entry.handler,
|
|
3337
|
-
derive,
|
|
3338
|
-
before,
|
|
3339
|
-
entry.schema?.query,
|
|
3340
|
-
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3341
|
-
)
|
|
3342
|
-
: laneName === "derive-before-after" &&
|
|
3343
|
-
derive !== undefined &&
|
|
3344
|
-
before !== undefined &&
|
|
3345
|
-
after !== undefined
|
|
3346
|
-
? this.buildFusedDeriveBeforeAfter(
|
|
3347
|
-
entry.handler,
|
|
3348
|
-
derive,
|
|
3349
|
-
before,
|
|
3350
|
-
after,
|
|
3351
|
-
entry.schema?.query,
|
|
3352
|
-
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3353
|
-
)
|
|
3354
|
-
: this.buildFusedWeb(
|
|
3355
|
-
entry.handler,
|
|
3356
|
-
entry.hasDecorations ? entry.decorations : undefined,
|
|
3357
|
-
isContextlessNoArgArrow(entry.handler),
|
|
3358
|
-
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3359
|
-
)
|
|
3360
3399
|
return {
|
|
3361
3400
|
...route,
|
|
3362
3401
|
entry: {
|
|
@@ -3391,7 +3430,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3391
3430
|
fetch(req: Request, platform?: Platform<EnvOf<Ctx>>): MaybePromise<Response> {
|
|
3392
3431
|
// Deno's `Request.url` is the target as sent; Bun, workerd and every `new Request()` parse it.
|
|
3393
3432
|
// Route what the parsed URL says, so `/users/../admin` is `/admin` on every runtime.
|
|
3394
|
-
const target = requestTarget(req)
|
|
3433
|
+
const target = requestTarget(req, this.validateHost, this.normalizeTarget)
|
|
3395
3434
|
if (target === true) return jsonError(400, "invalid_host")
|
|
3396
3435
|
if (target !== false) {
|
|
3397
3436
|
const rebuilt = withResolvedTarget(req, target)
|
|
@@ -3463,6 +3502,47 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3463
3502
|
)
|
|
3464
3503
|
}
|
|
3465
3504
|
|
|
3505
|
+
/** {@link fetchMatched} for an app whose `onRequest` hooks run before the route does: the hooks see
|
|
3506
|
+
* the request first, and the route Bun matched runs unless one of them replaced the request. */
|
|
3507
|
+
private fetchMatchedHooked(
|
|
3508
|
+
source: Request,
|
|
3509
|
+
entry: RouteEntry,
|
|
3510
|
+
params: Record<string, string>,
|
|
3511
|
+
platform: Platform<EnvOf<Ctx>>,
|
|
3512
|
+
): MaybePromise<Response> {
|
|
3513
|
+
if (this.capacityGate === undefined) {
|
|
3514
|
+
return this.fetchMatchedHookedInner(source, entry, params, platform)
|
|
3515
|
+
}
|
|
3516
|
+
return this.admitGated(source, () =>
|
|
3517
|
+
this.fetchMatchedHookedInner(source, entry, params, platform),
|
|
3518
|
+
)
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
private fetchMatchedHookedInner(
|
|
3522
|
+
source: Request,
|
|
3523
|
+
entry: RouteEntry,
|
|
3524
|
+
params: Record<string, string>,
|
|
3525
|
+
platform: Platform<EnvOf<Ctx>>,
|
|
3526
|
+
): MaybePromise<Response> {
|
|
3527
|
+
const outcome = this.runWithOnRequest<Response>(
|
|
3528
|
+
source,
|
|
3529
|
+
this.trustedPlatform(source, platform),
|
|
3530
|
+
this.finalizeResponse,
|
|
3531
|
+
this.wrapWebResponse,
|
|
3532
|
+
this.webResponseTimeout,
|
|
3533
|
+
true,
|
|
3534
|
+
{ entry, params },
|
|
3535
|
+
)
|
|
3536
|
+
if (this.onResponseHooks.length === 0 && this.onResponseFinalizedHooks.length === 0) {
|
|
3537
|
+
return outcome
|
|
3538
|
+
}
|
|
3539
|
+
return outcome instanceof Promise
|
|
3540
|
+
? outcome.then((response) =>
|
|
3541
|
+
this.applyOnResponseAndFinalize(response, this.takeResponseRequest(source)),
|
|
3542
|
+
)
|
|
3543
|
+
: this.applyOnResponseAndFinalize(outcome, this.takeResponseRequest(source))
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3466
3546
|
private fetchMatchedInner(
|
|
3467
3547
|
source: RequestSource,
|
|
3468
3548
|
entry: RouteEntry,
|
|
@@ -3895,7 +3975,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3895
3975
|
// The adapter answers a pass by handing this same object to `fetch`.
|
|
3896
3976
|
const received = req
|
|
3897
3977
|
// The handshake routes the path `fetch` would: Host checked, dot segments resolved (see `fetch`).
|
|
3898
|
-
const target = requestTarget(req)
|
|
3978
|
+
const target = requestTarget(req, this.validateHost, this.normalizeTarget)
|
|
3899
3979
|
if (target === true) {
|
|
3900
3980
|
return { kind: "reject", response: this.wrapWebResponse(jsonError(400, "invalid_host")) }
|
|
3901
3981
|
}
|
|
@@ -4116,10 +4196,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4116
4196
|
private bunFetchWithWebSocket(
|
|
4117
4197
|
req: Request,
|
|
4118
4198
|
server: BunUpgradeServer,
|
|
4199
|
+
setIdleTimeout: IdleTimeoutSetter,
|
|
4119
4200
|
): MaybePromise<Response | undefined> {
|
|
4120
4201
|
const handle = (o: WebSocketUpgradeOutcome): MaybePromise<Response | undefined> => {
|
|
4121
4202
|
if (o.kind === "pass")
|
|
4122
|
-
return this.fetch(
|
|
4203
|
+
return this.fetch(
|
|
4204
|
+
req,
|
|
4205
|
+
new BunPeerPlatform(server, req, setIdleTimeout) as Platform<EnvOf<Ctx>>,
|
|
4206
|
+
)
|
|
4123
4207
|
if (o.kind === "reject") return o.response
|
|
4124
4208
|
// A copy: the Bun socket keeps per-connection state on it, and a hand-built outcome may be shared.
|
|
4125
4209
|
return server.upgrade(req, { data: { ...o } })
|
|
@@ -4128,7 +4212,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4128
4212
|
}
|
|
4129
4213
|
const outcome = this.resolveWebSocketUpgrade(
|
|
4130
4214
|
req,
|
|
4131
|
-
|
|
4215
|
+
new BunPeerPlatform(server, req, setIdleTimeout) as Platform<EnvOf<Ctx>>,
|
|
4132
4216
|
)
|
|
4133
4217
|
return outcome instanceof Promise ? outcome.then(handle) : handle(outcome)
|
|
4134
4218
|
}
|
|
@@ -4281,6 +4365,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4281
4365
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4282
4366
|
onTimeout: () => T,
|
|
4283
4367
|
webFast: boolean,
|
|
4368
|
+
matched?: MatchedRoute,
|
|
4284
4369
|
): MaybePromise<T> {
|
|
4285
4370
|
const hooks = this.onRequestHooks
|
|
4286
4371
|
const originalRequest = requestOf(source)
|
|
@@ -4310,6 +4395,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4310
4395
|
wrapResponse,
|
|
4311
4396
|
onTimeout,
|
|
4312
4397
|
webFast,
|
|
4398
|
+
matched,
|
|
4313
4399
|
),
|
|
4314
4400
|
(err) => this.onRequestFailure(err, current, platform, finalize, wrapResponse),
|
|
4315
4401
|
)
|
|
@@ -4322,7 +4408,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4322
4408
|
}
|
|
4323
4409
|
if (outcome !== undefined) return earlyAnswer(outcome, wrapResponse)
|
|
4324
4410
|
}
|
|
4325
|
-
return this.routeAndRun(
|
|
4411
|
+
return this.routeAndRun(
|
|
4412
|
+
current,
|
|
4413
|
+
platform,
|
|
4414
|
+
finalize,
|
|
4415
|
+
wrapResponse,
|
|
4416
|
+
onTimeout,
|
|
4417
|
+
webFast,
|
|
4418
|
+
current === originalRequest ? matched : undefined,
|
|
4419
|
+
)
|
|
4326
4420
|
}
|
|
4327
4421
|
|
|
4328
4422
|
/** Async tail of {@link runWithOnRequest}: applies the first awaited hook's outcome, then runs
|
|
@@ -4337,6 +4431,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4337
4431
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4338
4432
|
onTimeout: () => T,
|
|
4339
4433
|
webFast: boolean,
|
|
4434
|
+
matched?: MatchedRoute,
|
|
4340
4435
|
): Promise<T> {
|
|
4341
4436
|
let current = sourceAtAwait
|
|
4342
4437
|
let seen = requestOf(sourceAtAwait)
|
|
@@ -4360,7 +4455,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4360
4455
|
}
|
|
4361
4456
|
index++
|
|
4362
4457
|
}
|
|
4363
|
-
return this.routeAndRun(
|
|
4458
|
+
return this.routeAndRun(
|
|
4459
|
+
current,
|
|
4460
|
+
platform,
|
|
4461
|
+
finalize,
|
|
4462
|
+
wrapResponse,
|
|
4463
|
+
onTimeout,
|
|
4464
|
+
webFast,
|
|
4465
|
+
current === originalRequest ? matched : undefined,
|
|
4466
|
+
)
|
|
4364
4467
|
}
|
|
4365
4468
|
|
|
4366
4469
|
private takeResponseRequest(source: RequestSource): Request {
|
|
@@ -4438,7 +4541,22 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4438
4541
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4439
4542
|
onTimeout: () => T,
|
|
4440
4543
|
webFast: boolean,
|
|
4544
|
+
// The route Bun's table already matched for this very request, which routing would find again.
|
|
4545
|
+
matched?: MatchedRoute,
|
|
4441
4546
|
): MaybePromise<T> {
|
|
4547
|
+
if (matched !== undefined) {
|
|
4548
|
+
return this.runMatched(
|
|
4549
|
+
source,
|
|
4550
|
+
platform,
|
|
4551
|
+
matched.entry,
|
|
4552
|
+
matched.params,
|
|
4553
|
+
undefined,
|
|
4554
|
+
finalize,
|
|
4555
|
+
wrapResponse,
|
|
4556
|
+
onTimeout,
|
|
4557
|
+
webFast,
|
|
4558
|
+
)
|
|
4559
|
+
}
|
|
4442
4560
|
// Native delimiter searches keep path/query splitting out of a character-by-character JS loop.
|
|
4443
4561
|
// Keep the slices in locals instead of allocating the public helper's pathname/search pair.
|
|
4444
4562
|
let pathname: string
|
|
@@ -5287,131 +5405,298 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5287
5405
|
}
|
|
5288
5406
|
|
|
5289
5407
|
/**
|
|
5290
|
-
*
|
|
5291
|
-
*
|
|
5292
|
-
*
|
|
5293
|
-
*
|
|
5294
|
-
* and
|
|
5295
|
-
* hot hidden class. Behavior is byte-identical to the compiled route program: the same thrown-value
|
|
5296
|
-
* contract (`Response` is control flow, anything else logs + 500), the same `c.set` semantics, the
|
|
5297
|
-
* same `fusedRespond` for the rendered body. The `LifecycleExecutionLane` guarantee
|
|
5298
|
-
* (`schema.params === undefined && schema.headers === undefined && derives === 1 &&
|
|
5299
|
-
* beforeHandle === 1 && afterHandle === 0 && onError === 0 && !hasResponseContract &&
|
|
5300
|
-
* !hasDecorations`) is enforced by `selectRouteLanes`; this lane is selected ONLY when all of those
|
|
5301
|
-
* hold. A `query` schema (the route declares `{ query }` alongside the hooks) is validated FIRST,
|
|
5302
|
-
* mirroring the compiled route program's stage order; the fused builder runs validate → derive →
|
|
5303
|
-
* before → handler in one frame.
|
|
5408
|
+
* The stage chain a hook shape runs - derive, before, handler, after, respond - for the Web lane,
|
|
5409
|
+
* built once per server and shared by every route with the same number of hooks in each phase (none,
|
|
5410
|
+
* one or several). The chain reads a route's handler and hooks from its `FusedHooks` per request, so
|
|
5411
|
+
* a route's own closure (see {@link buildFusedLifecycleWeb}) holds only that record and its schemas,
|
|
5412
|
+
* and memory grows with the number of distinct shapes, not routes or hook functions.
|
|
5304
5413
|
*/
|
|
5305
|
-
private
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5414
|
+
private fusedWebStages(
|
|
5415
|
+
deriveCount: HookCount,
|
|
5416
|
+
beforeCount: HookCount,
|
|
5417
|
+
afterCount: HookCount,
|
|
5418
|
+
): FusedWebStart {
|
|
5419
|
+
// A shape creates only the stages it uses. Which stage follows which is settled once here, so a
|
|
5420
|
+
// request makes one call per hook and per phase. A phase with a single hook, the common middleware
|
|
5421
|
+
// shape, gets a straight-line closure that reads that hook from the route; a phase with several
|
|
5422
|
+
// loops over the route's array, running synchronous hooks in place so the stack stays flat however
|
|
5423
|
+
// many a phase holds, and only a hook that returns a promise re-enters its stage from a microtask.
|
|
5424
|
+
// Each request supplies its own context and route.
|
|
5425
|
+
type Stage = (ctx: RawContext, route: FusedHooks, index: number) => MaybePromise<Response>
|
|
5426
|
+
type AfterStage = (
|
|
5427
|
+
result: unknown,
|
|
5428
|
+
ctx: RawContext,
|
|
5429
|
+
route: FusedHooks,
|
|
5430
|
+
index: number,
|
|
5431
|
+
) => MaybePromise<Response>
|
|
5432
|
+
let runAfter: AfterStage
|
|
5433
|
+
if (afterCount === 0) {
|
|
5434
|
+
runAfter = (result, ctx, route) =>
|
|
5435
|
+
this.respondHooks(result, ctx, route, this.onResponseHooks.length === 0)
|
|
5436
|
+
} else if (afterCount === 1) {
|
|
5437
|
+
runAfter = (result, ctx, route) => {
|
|
5438
|
+
let transformed: unknown
|
|
5439
|
+
try {
|
|
5440
|
+
transformed = route.afters[0]!(result, ctx)
|
|
5441
|
+
} catch (err) {
|
|
5442
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5443
|
+
}
|
|
5444
|
+
if (transformed instanceof Promise) {
|
|
5445
|
+
return transformed.then(
|
|
5446
|
+
(value) => this.respondHooks(value, ctx, route, this.onResponseHooks.length === 0),
|
|
5447
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5448
|
+
)
|
|
5449
|
+
}
|
|
5450
|
+
return this.respondHooks(transformed, ctx, route, this.onResponseHooks.length === 0)
|
|
5451
|
+
}
|
|
5452
|
+
} else {
|
|
5453
|
+
runAfter = (result, ctx, route, index) => {
|
|
5454
|
+
const lastAfter = route.afters.length - 1
|
|
5455
|
+
for (;;) {
|
|
5456
|
+
let transformed: unknown
|
|
5457
|
+
try {
|
|
5458
|
+
transformed = route.afters[index]!(result, ctx)
|
|
5459
|
+
} catch (err) {
|
|
5460
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5461
|
+
}
|
|
5462
|
+
if (transformed instanceof Promise) {
|
|
5463
|
+
return transformed.then(
|
|
5464
|
+
(value) =>
|
|
5465
|
+
index < lastAfter
|
|
5466
|
+
? runAfter(value, ctx, route, index + 1)
|
|
5467
|
+
: this.respondHooks(value, ctx, route, this.onResponseHooks.length === 0),
|
|
5468
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5469
|
+
)
|
|
5470
|
+
}
|
|
5471
|
+
if (index === lastAfter) {
|
|
5472
|
+
return this.respondHooks(transformed, ctx, route, this.onResponseHooks.length === 0)
|
|
5473
|
+
}
|
|
5474
|
+
result = transformed
|
|
5475
|
+
index++
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
const runHandler: Stage = (ctx, route) => {
|
|
5480
|
+
let result: unknown
|
|
5481
|
+
try {
|
|
5482
|
+
result = route.handler(ctx)
|
|
5483
|
+
} catch (err) {
|
|
5484
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5485
|
+
}
|
|
5486
|
+
if (result instanceof Promise) {
|
|
5487
|
+
return result.then(
|
|
5488
|
+
(value) => runAfter(value, ctx, route, 0),
|
|
5489
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5490
|
+
)
|
|
5491
|
+
}
|
|
5492
|
+
return runAfter(result, ctx, route, 0)
|
|
5493
|
+
}
|
|
5494
|
+
let runBefore: Stage
|
|
5495
|
+
if (beforeCount === 0) {
|
|
5496
|
+
runBefore = runHandler
|
|
5497
|
+
} else if (beforeCount === 1) {
|
|
5498
|
+
runBefore = (ctx, route) => {
|
|
5499
|
+
let early: unknown
|
|
5500
|
+
try {
|
|
5501
|
+
early = route.befores[0]!(ctx)
|
|
5502
|
+
} catch (err) {
|
|
5503
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5504
|
+
}
|
|
5505
|
+
if (early instanceof Promise) {
|
|
5506
|
+
return early.then(
|
|
5507
|
+
(settled) =>
|
|
5508
|
+
settled === undefined
|
|
5509
|
+
? runHandler(ctx, route, 0)
|
|
5510
|
+
: this.respondHooks(settled, ctx, route),
|
|
5511
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5512
|
+
)
|
|
5513
|
+
}
|
|
5514
|
+
if (early === undefined) return runHandler(ctx, route, 0)
|
|
5515
|
+
return this.respondHooks(early, ctx, route)
|
|
5516
|
+
}
|
|
5517
|
+
} else {
|
|
5518
|
+
runBefore = (ctx, route, index) => {
|
|
5519
|
+
const lastBefore = route.befores.length - 1
|
|
5520
|
+
for (;;) {
|
|
5521
|
+
let early: unknown
|
|
5522
|
+
try {
|
|
5523
|
+
early = route.befores[index]!(ctx)
|
|
5524
|
+
} catch (err) {
|
|
5525
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5526
|
+
}
|
|
5527
|
+
if (early instanceof Promise) {
|
|
5528
|
+
return early.then(
|
|
5529
|
+
(settled) => {
|
|
5530
|
+
if (settled !== undefined) return this.respondHooks(settled, ctx, route)
|
|
5531
|
+
return index < lastBefore
|
|
5532
|
+
? runBefore(ctx, route, index + 1)
|
|
5533
|
+
: runHandler(ctx, route, 0)
|
|
5534
|
+
},
|
|
5535
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5536
|
+
)
|
|
5537
|
+
}
|
|
5538
|
+
if (early !== undefined) return this.respondHooks(early, ctx, route)
|
|
5539
|
+
if (index === lastBefore) return runHandler(ctx, route, 0)
|
|
5540
|
+
index++
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
let runDerive: Stage
|
|
5545
|
+
if (deriveCount === 0) {
|
|
5546
|
+
runDerive = runBefore
|
|
5547
|
+
} else if (deriveCount === 1) {
|
|
5548
|
+
runDerive = (ctx, route) => {
|
|
5549
|
+
let derived: unknown
|
|
5550
|
+
try {
|
|
5551
|
+
derived = route.derives[0]!(ctx)
|
|
5552
|
+
} catch (err) {
|
|
5553
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5554
|
+
}
|
|
5555
|
+
if (derived instanceof Promise) {
|
|
5556
|
+
return derived.then(
|
|
5557
|
+
(settled) => {
|
|
5558
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
5559
|
+
return this.respondHooks(settled, ctx, route)
|
|
5560
|
+
}
|
|
5561
|
+
try {
|
|
5562
|
+
Object.assign(ctx, settled)
|
|
5563
|
+
} catch (err) {
|
|
5564
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5565
|
+
}
|
|
5566
|
+
return runBefore(ctx, route, 0)
|
|
5567
|
+
},
|
|
5568
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5569
|
+
)
|
|
5570
|
+
}
|
|
5571
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
5572
|
+
return this.respondHooks(derived, ctx, route)
|
|
5573
|
+
}
|
|
5574
|
+
try {
|
|
5575
|
+
Object.assign(ctx, derived)
|
|
5576
|
+
} catch (err) {
|
|
5577
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5578
|
+
}
|
|
5579
|
+
return runBefore(ctx, route, 0)
|
|
5580
|
+
}
|
|
5581
|
+
} else {
|
|
5582
|
+
runDerive = (ctx, route, index) => {
|
|
5583
|
+
const lastDerive = route.derives.length - 1
|
|
5584
|
+
for (;;) {
|
|
5585
|
+
let derived: unknown
|
|
5586
|
+
try {
|
|
5587
|
+
derived = route.derives[index]!(ctx)
|
|
5588
|
+
} catch (err) {
|
|
5589
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5590
|
+
}
|
|
5591
|
+
if (derived instanceof Promise) {
|
|
5592
|
+
return derived.then(
|
|
5593
|
+
(settled) => {
|
|
5594
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
5595
|
+
return this.respondHooks(settled, ctx, route)
|
|
5596
|
+
}
|
|
5597
|
+
try {
|
|
5598
|
+
Object.assign(ctx, settled)
|
|
5599
|
+
} catch (err) {
|
|
5600
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5601
|
+
}
|
|
5602
|
+
return index < lastDerive
|
|
5603
|
+
? runDerive(ctx, route, index + 1)
|
|
5604
|
+
: runBefore(ctx, route, 0)
|
|
5605
|
+
},
|
|
5606
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5607
|
+
)
|
|
5608
|
+
}
|
|
5609
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
5610
|
+
return this.respondHooks(derived, ctx, route)
|
|
5611
|
+
}
|
|
5612
|
+
try {
|
|
5613
|
+
Object.assign(ctx, derived)
|
|
5614
|
+
} catch (err) {
|
|
5615
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5616
|
+
}
|
|
5617
|
+
if (index === lastDerive) return runBefore(ctx, route, 0)
|
|
5618
|
+
index++
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
return runDerive
|
|
5320
5623
|
}
|
|
5321
5624
|
|
|
5322
|
-
|
|
5323
|
-
|
|
5625
|
+
/**
|
|
5626
|
+
* Compile a fused lifecycle route's entry closure: parse (body) → validate → the shared stage chain
|
|
5627
|
+
* of its hook shape (derive → before → handler → after → respond, see {@link fusedWebStages}) in a
|
|
5628
|
+
* single frame, where the generic program walks staged hook loops. `selectRouteLanes` admits only
|
|
5629
|
+
* derive, before and after hooks - no params or headers schema, onError, around, decorations,
|
|
5630
|
+
* response contract or recovery hook, and never a query schema beside a body schema - so the behavior
|
|
5631
|
+
* is the compiled program's: the same stage order, the same thrown-value contract (`Response` is
|
|
5632
|
+
* control flow, anything else logs and 500s), the same `c.set` semantics and `fusedRespond` render.
|
|
5633
|
+
* The route's own closure holds only its `FusedHooks` and schemas and takes the request's context as
|
|
5634
|
+
* an argument, so a request allocates no step closures.
|
|
5635
|
+
*/
|
|
5636
|
+
/**
|
|
5637
|
+
* Keep the common one-derive/one-before shape bound to its hooks. Inline its synchronous path to
|
|
5638
|
+
* avoid stage calls; retain helper closures only for promise continuations.
|
|
5639
|
+
*/
|
|
5640
|
+
private buildFusedLifecycleWebSingle(
|
|
5324
5641
|
handler: InternalHandler,
|
|
5325
5642
|
derive: RawDerive,
|
|
5326
5643
|
before: RawBeforeHandle,
|
|
5327
5644
|
after: RawAfterHandle | undefined,
|
|
5328
|
-
querySchema: StandardSchemaV1 | undefined,
|
|
5329
5645
|
maxBodyBytes: number,
|
|
5330
5646
|
): FusedWebRunner {
|
|
5331
|
-
const
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
(result,
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
(err) => logError(err, ctx),
|
|
5352
|
-
)
|
|
5353
|
-
}
|
|
5354
|
-
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0)
|
|
5355
|
-
}
|
|
5647
|
+
const runAfter =
|
|
5648
|
+
after === undefined
|
|
5649
|
+
? (result: unknown, ctx: RawContext): MaybePromise<Response> =>
|
|
5650
|
+
this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
5651
|
+
: (result: unknown, ctx: RawContext): MaybePromise<Response> => {
|
|
5652
|
+
let transformed: unknown
|
|
5653
|
+
try {
|
|
5654
|
+
transformed = after(result, ctx)
|
|
5655
|
+
} catch (err) {
|
|
5656
|
+
return this.fusedWebFail(err, ctx)
|
|
5657
|
+
}
|
|
5658
|
+
if (transformed instanceof Promise) {
|
|
5659
|
+
return transformed.then(
|
|
5660
|
+
(value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0),
|
|
5661
|
+
(err) => this.fusedWebFail(err, ctx),
|
|
5662
|
+
)
|
|
5663
|
+
}
|
|
5664
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0)
|
|
5665
|
+
}
|
|
5666
|
+
|
|
5356
5667
|
const runHandler = (ctx: RawContext): MaybePromise<Response> => {
|
|
5357
5668
|
let result: unknown
|
|
5358
5669
|
try {
|
|
5359
5670
|
result = handler(ctx)
|
|
5360
5671
|
} catch (err) {
|
|
5361
|
-
return
|
|
5672
|
+
return this.fusedWebFail(err, ctx)
|
|
5362
5673
|
}
|
|
5363
5674
|
if (result instanceof Promise) {
|
|
5364
5675
|
return result.then(
|
|
5365
5676
|
(value) => runAfter(value, ctx),
|
|
5366
|
-
(err) =>
|
|
5677
|
+
(err) => this.fusedWebFail(err, ctx),
|
|
5367
5678
|
)
|
|
5368
5679
|
}
|
|
5369
5680
|
return runAfter(result, ctx)
|
|
5370
5681
|
}
|
|
5682
|
+
|
|
5371
5683
|
const runBefore = (ctx: RawContext): MaybePromise<Response> => {
|
|
5372
5684
|
let early: unknown
|
|
5373
5685
|
try {
|
|
5374
5686
|
early = before(ctx)
|
|
5375
5687
|
} catch (err) {
|
|
5376
|
-
return
|
|
5688
|
+
return this.fusedWebFail(err, ctx)
|
|
5377
5689
|
}
|
|
5378
5690
|
if (early instanceof Promise) {
|
|
5379
5691
|
return early.then(
|
|
5380
|
-
(settled) =>
|
|
5381
|
-
|
|
5382
|
-
return this.respondFused(settled, ctx)
|
|
5383
|
-
},
|
|
5384
|
-
(err) => logError(err, ctx),
|
|
5692
|
+
(settled) => (settled === undefined ? runHandler(ctx) : this.respondFused(settled, ctx)),
|
|
5693
|
+
(err) => this.fusedWebFail(err, ctx),
|
|
5385
5694
|
)
|
|
5386
5695
|
}
|
|
5387
5696
|
if (early === undefined) return runHandler(ctx)
|
|
5388
5697
|
return this.respondFused(early, ctx)
|
|
5389
5698
|
}
|
|
5390
|
-
|
|
5391
|
-
let derived: unknown
|
|
5392
|
-
try {
|
|
5393
|
-
derived = derive(ctx)
|
|
5394
|
-
} catch (err) {
|
|
5395
|
-
return logError(err, ctx)
|
|
5396
|
-
}
|
|
5397
|
-
if (derived instanceof Promise) {
|
|
5398
|
-
return derived.then(
|
|
5399
|
-
(settled) => {
|
|
5400
|
-
if (isResponseResult(settled) || settled instanceof Response) {
|
|
5401
|
-
return this.respondFused(settled, ctx)
|
|
5402
|
-
}
|
|
5403
|
-
Object.assign(ctx, settled as object)
|
|
5404
|
-
return runBefore(ctx)
|
|
5405
|
-
},
|
|
5406
|
-
(err) => logError(err, ctx),
|
|
5407
|
-
)
|
|
5408
|
-
}
|
|
5409
|
-
if (isResponseResult(derived) || derived instanceof Response) {
|
|
5410
|
-
return this.respondFused(derived, ctx)
|
|
5411
|
-
}
|
|
5412
|
-
Object.assign(ctx, derived as object)
|
|
5413
|
-
return runBefore(ctx)
|
|
5414
|
-
}
|
|
5699
|
+
|
|
5415
5700
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
5416
5701
|
const ctx = nativeContext
|
|
5417
5702
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
@@ -5425,154 +5710,113 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5425
5710
|
maxBodyBytes,
|
|
5426
5711
|
this.protoPoisoning,
|
|
5427
5712
|
)
|
|
5428
|
-
|
|
5429
|
-
let
|
|
5713
|
+
|
|
5714
|
+
let derived: unknown
|
|
5430
5715
|
try {
|
|
5431
|
-
|
|
5716
|
+
derived = derive(ctx)
|
|
5432
5717
|
} catch (err) {
|
|
5433
|
-
return
|
|
5718
|
+
return this.fusedWebFail(err, ctx)
|
|
5434
5719
|
}
|
|
5435
|
-
if (
|
|
5436
|
-
return
|
|
5720
|
+
if (derived instanceof Promise) {
|
|
5721
|
+
return derived.then(
|
|
5437
5722
|
(settled) => {
|
|
5438
|
-
if (settled
|
|
5439
|
-
return this.
|
|
5440
|
-
|
|
5441
|
-
|
|
5723
|
+
if (isResponseResult(settled) || settled instanceof Response)
|
|
5724
|
+
return this.respondFused(settled, ctx)
|
|
5725
|
+
try {
|
|
5726
|
+
Object.assign(ctx, settled)
|
|
5727
|
+
} catch (err) {
|
|
5728
|
+
return this.fusedWebFail(err, ctx)
|
|
5729
|
+
}
|
|
5730
|
+
return runBefore(ctx)
|
|
5442
5731
|
},
|
|
5443
|
-
(err) =>
|
|
5732
|
+
(err) => this.fusedWebFail(err, ctx),
|
|
5444
5733
|
)
|
|
5445
5734
|
}
|
|
5446
|
-
if (
|
|
5447
|
-
return this.
|
|
5448
|
-
|
|
5449
|
-
|
|
5735
|
+
if (isResponseResult(derived) || derived instanceof Response)
|
|
5736
|
+
return this.respondFused(derived, ctx)
|
|
5737
|
+
|
|
5738
|
+
let early: unknown
|
|
5739
|
+
let result: unknown
|
|
5740
|
+
let transformed: unknown
|
|
5741
|
+
let handlerPromise: Promise<unknown> | undefined
|
|
5742
|
+
try {
|
|
5743
|
+
Object.assign(ctx, derived)
|
|
5744
|
+
early = before(ctx)
|
|
5745
|
+
if (early === undefined) {
|
|
5746
|
+
result = handler(ctx)
|
|
5747
|
+
if (result instanceof Promise) handlerPromise = result
|
|
5748
|
+
else if (after !== undefined) transformed = after(result, ctx)
|
|
5749
|
+
}
|
|
5750
|
+
} catch (err) {
|
|
5751
|
+
return this.fusedWebFail(err, ctx)
|
|
5752
|
+
}
|
|
5753
|
+
|
|
5754
|
+
if (early instanceof Promise) {
|
|
5755
|
+
return early.then(
|
|
5756
|
+
(settled) => (settled === undefined ? runHandler(ctx) : this.respondFused(settled, ctx)),
|
|
5757
|
+
(err) => this.fusedWebFail(err, ctx),
|
|
5758
|
+
)
|
|
5759
|
+
}
|
|
5760
|
+
if (early !== undefined) return this.respondFused(early, ctx)
|
|
5761
|
+
if (handlerPromise !== undefined) {
|
|
5762
|
+
return handlerPromise.then(
|
|
5763
|
+
(value) =>
|
|
5764
|
+
after === undefined
|
|
5765
|
+
? this.respondFused(value, ctx, this.onResponseHooks.length === 0)
|
|
5766
|
+
: runAfter(value, ctx),
|
|
5767
|
+
(err) => this.fusedWebFail(err, ctx),
|
|
5768
|
+
)
|
|
5769
|
+
}
|
|
5770
|
+
if (after === undefined)
|
|
5771
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
5772
|
+
if (transformed instanceof Promise) {
|
|
5773
|
+
return transformed.then(
|
|
5774
|
+
(value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0),
|
|
5775
|
+
(err) => this.fusedWebFail(err, ctx),
|
|
5776
|
+
)
|
|
5777
|
+
}
|
|
5778
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0)
|
|
5450
5779
|
}
|
|
5451
5780
|
}
|
|
5452
5781
|
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
* ONLY when `schema.body !== undefined && schema.query/params/headers === undefined &&
|
|
5458
|
-
* schema.onValidationError === undefined && !defaultOnValidationError && !hasIdempotency &&
|
|
5459
|
-
* !hasLedger && derives === 1 && beforeHandle === 1 && afterHandle <= 1 && onError === 0 &&
|
|
5460
|
-
* !hasResponseContract && !hasDecorations && around === 0`. The same single `querySchema === undefined`
|
|
5461
|
-
* trick used in the no-body lane lets the build path stay simple: a query + body route falls back
|
|
5462
|
-
* to the generic program (this is rare in practice - validated query + validated body is usually a
|
|
5463
|
-
* "search results" route with a middleware chain on top, and the generic lane is fine there).
|
|
5464
|
-
*/
|
|
5465
|
-
private buildFusedBodyDeriveBeforeAfter(
|
|
5466
|
-
handler: InternalHandler,
|
|
5467
|
-
derive: RawDerive,
|
|
5468
|
-
before: RawBeforeHandle,
|
|
5469
|
-
after: RawAfterHandle | undefined,
|
|
5470
|
-
bodySchema: StandardSchemaV1,
|
|
5782
|
+
private buildFusedLifecycleWeb(
|
|
5783
|
+
route: FusedHooks,
|
|
5784
|
+
querySchema: StandardSchemaV1 | undefined,
|
|
5785
|
+
bodySchema: StandardSchemaV1 | undefined,
|
|
5471
5786
|
maxBodyBytes: number,
|
|
5472
5787
|
): FusedWebRunner {
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
)
|
|
5493
|
-
const runHandlerValidated = (): MaybePromise<Response> => {
|
|
5494
|
-
const runAfter = (result: unknown): MaybePromise<Response> => {
|
|
5495
|
-
if (after === undefined) {
|
|
5496
|
-
return this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
5497
|
-
}
|
|
5498
|
-
let transformed: unknown
|
|
5499
|
-
try {
|
|
5500
|
-
transformed = after(result, ctx)
|
|
5501
|
-
} catch (err) {
|
|
5502
|
-
return logError(err, ctx)
|
|
5503
|
-
}
|
|
5504
|
-
if (transformed instanceof Promise) {
|
|
5505
|
-
return transformed.then(
|
|
5506
|
-
(value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0),
|
|
5507
|
-
(err) => logError(err, ctx),
|
|
5508
|
-
)
|
|
5509
|
-
}
|
|
5510
|
-
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0)
|
|
5511
|
-
}
|
|
5512
|
-
const runHandler = (): MaybePromise<Response> => {
|
|
5513
|
-
let result: unknown
|
|
5514
|
-
try {
|
|
5515
|
-
result = handler(ctx)
|
|
5516
|
-
} catch (err) {
|
|
5517
|
-
return logError(err, ctx)
|
|
5518
|
-
}
|
|
5519
|
-
if (result instanceof Promise) {
|
|
5520
|
-
return result.then(
|
|
5521
|
-
(value) => runAfter(value),
|
|
5522
|
-
(err) => logError(err, ctx),
|
|
5523
|
-
)
|
|
5524
|
-
}
|
|
5525
|
-
return runAfter(result)
|
|
5526
|
-
}
|
|
5527
|
-
const runBefore = (): MaybePromise<Response> => {
|
|
5528
|
-
let early: unknown
|
|
5529
|
-
try {
|
|
5530
|
-
early = before(ctx)
|
|
5531
|
-
} catch (err) {
|
|
5532
|
-
return logError(err, ctx)
|
|
5533
|
-
}
|
|
5534
|
-
if (early instanceof Promise) {
|
|
5535
|
-
return early.then(
|
|
5536
|
-
(settled) => {
|
|
5537
|
-
if (settled === undefined) return runHandler()
|
|
5538
|
-
return this.respondFused(settled, ctx)
|
|
5539
|
-
},
|
|
5540
|
-
(err) => logError(err, ctx),
|
|
5541
|
-
)
|
|
5542
|
-
}
|
|
5543
|
-
if (early === undefined) return runHandler()
|
|
5544
|
-
return this.respondFused(early, ctx)
|
|
5545
|
-
}
|
|
5546
|
-
let derived: unknown
|
|
5547
|
-
try {
|
|
5548
|
-
derived = derive(ctx)
|
|
5549
|
-
} catch (err) {
|
|
5550
|
-
return logError(err, ctx)
|
|
5551
|
-
}
|
|
5552
|
-
if (derived instanceof Promise) {
|
|
5553
|
-
return derived.then(
|
|
5554
|
-
(settled) => {
|
|
5555
|
-
if (isResponseResult(settled) || settled instanceof Response) {
|
|
5556
|
-
return this.respondFused(settled, ctx)
|
|
5557
|
-
}
|
|
5558
|
-
Object.assign(ctx, settled as object)
|
|
5559
|
-
return runBefore()
|
|
5560
|
-
},
|
|
5561
|
-
(err) => logError(err, ctx),
|
|
5562
|
-
)
|
|
5563
|
-
}
|
|
5564
|
-
if (isResponseResult(derived) || derived instanceof Response) {
|
|
5565
|
-
return this.respondFused(derived, ctx)
|
|
5566
|
-
}
|
|
5567
|
-
Object.assign(ctx, derived as object)
|
|
5568
|
-
return runBefore()
|
|
5788
|
+
if (
|
|
5789
|
+
querySchema === undefined &&
|
|
5790
|
+
bodySchema === undefined &&
|
|
5791
|
+
route.errors.length === 0 &&
|
|
5792
|
+
route.decorations === undefined &&
|
|
5793
|
+
route.derives.length === 1 &&
|
|
5794
|
+
route.befores.length === 1 &&
|
|
5795
|
+
route.afters.length <= 1
|
|
5796
|
+
) {
|
|
5797
|
+
const derive = route.derives[0]
|
|
5798
|
+
const before = route.befores[0]
|
|
5799
|
+
if (derive !== undefined && before !== undefined) {
|
|
5800
|
+
return this.buildFusedLifecycleWebSingle(
|
|
5801
|
+
route.handler,
|
|
5802
|
+
derive,
|
|
5803
|
+
before,
|
|
5804
|
+
route.afters[0],
|
|
5805
|
+
maxBodyBytes,
|
|
5806
|
+
)
|
|
5569
5807
|
}
|
|
5570
|
-
|
|
5808
|
+
}
|
|
5809
|
+
const runDerive = sharedStages(WEB_STAGES, this, route, (d, b, a) =>
|
|
5810
|
+
this.fusedWebStages(d, b, a),
|
|
5811
|
+
)
|
|
5812
|
+
const decorations = route.decorations
|
|
5813
|
+
if (bodySchema !== undefined) {
|
|
5814
|
+
const onParsed = (parsed: unknown, ctx: RawContext): MaybePromise<Response> => {
|
|
5571
5815
|
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
5572
5816
|
try {
|
|
5573
5817
|
validation = bodySchema["~standard"].validate(parsed)
|
|
5574
5818
|
} catch (err) {
|
|
5575
|
-
return
|
|
5819
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5576
5820
|
}
|
|
5577
5821
|
if (validation instanceof Promise) {
|
|
5578
5822
|
return validation.then(
|
|
@@ -5580,224 +5824,50 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5580
5824
|
if (settled.issues !== undefined)
|
|
5581
5825
|
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
5582
5826
|
ctx.body = settled.value
|
|
5583
|
-
return
|
|
5827
|
+
return runDerive(ctx, route, 0)
|
|
5584
5828
|
},
|
|
5585
|
-
(err) =>
|
|
5829
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5586
5830
|
)
|
|
5587
5831
|
}
|
|
5588
5832
|
if (validation.issues !== undefined)
|
|
5589
5833
|
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
5590
5834
|
ctx.body = validation.value
|
|
5591
|
-
return
|
|
5592
|
-
}
|
|
5593
|
-
return readBodyFramed(
|
|
5594
|
-
source,
|
|
5595
|
-
maxBodyBytes,
|
|
5596
|
-
this.protoPoisoning,
|
|
5597
|
-
onParsed,
|
|
5598
|
-
(response) => this.wrapWebResponse(response),
|
|
5599
|
-
(err) => Promise.resolve(logError(err, ctx)),
|
|
5600
|
-
bodySchema,
|
|
5601
|
-
this.formOrigins,
|
|
5602
|
-
) as MaybePromise<Response>
|
|
5603
|
-
}
|
|
5604
|
-
}
|
|
5605
|
-
|
|
5606
|
-
/**
|
|
5607
|
-
* Generic (finalize/wrapResponse-parameterized) fused runner for the derive-before(-after) lane:
|
|
5608
|
-
* the Node-direct counterpart of `buildFusedDeriveBefore` / `buildFusedDeriveBeforeAfter`.
|
|
5609
|
-
*
|
|
5610
|
-
* The Web fused builders above return a `Response`, so the Node dispatcher (`webFast === false`)
|
|
5611
|
-
* cannot use them and a derive+before route falls back to the generic route program there - two
|
|
5612
|
-
* staged loops, per-stage dispatch, and the async-continuation machinery - while Bun rides the
|
|
5613
|
-
* single closure. That asymmetry is the realistic-route Node deficit against Fastify: this runner
|
|
5614
|
-
* closes it by running validate → derive → before → handler → (after) in one frame and finalizing
|
|
5615
|
-
* through the caller's own `finalize` (the Node outcome on the direct lane, `toResponse` on Web),
|
|
5616
|
-
* so the same registration feeds both runtimes with no per-lane copy of the lifecycle to drift.
|
|
5617
|
-
*
|
|
5618
|
-
* Semantics mirror the generic program stage-for-stage for the shapes this lane admits (exactly
|
|
5619
|
-
* one derive, one beforeHandle, zero-or-one afterHandle, no onError/around/decorations/contracts/
|
|
5620
|
-
* recovery hooks - enforced by `selectRouteLanes`): thrown `Response`/`status(...)` are control
|
|
5621
|
-
* flow through `wrapResponse`/`finalize`, anything else logs + 500s via the shared `bareError`
|
|
5622
|
-
* lane (identical to `handleLifecycleError` with no `onError` hooks), and a terminal `finalize`
|
|
5623
|
-
* throw (e.g. an unstringifiable handler return on the Node lane) is caught exactly as
|
|
5624
|
-
* `finishLifecycleContract` catches it. Step functions close over registration state only and take
|
|
5625
|
-
* the request state as arguments, so the hot path allocates no per-request closures.
|
|
5626
|
-
*/
|
|
5627
|
-
private buildFusedDeriveBeforeRunner(
|
|
5628
|
-
handler: InternalHandler,
|
|
5629
|
-
derive: RawDerive,
|
|
5630
|
-
before: RawBeforeHandle,
|
|
5631
|
-
after: RawAfterHandle | undefined,
|
|
5632
|
-
querySchema: StandardSchemaV1 | undefined,
|
|
5633
|
-
maxBodyBytes: number,
|
|
5634
|
-
): FusedBodyRunner {
|
|
5635
|
-
const logError = <T>(
|
|
5636
|
-
err: unknown,
|
|
5637
|
-
ctx: RawContext,
|
|
5638
|
-
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
5639
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5640
|
-
): T => this.bareError(err, ctx, finalize, wrapResponse)
|
|
5641
|
-
|
|
5642
|
-
const runFinish = <T>(
|
|
5643
|
-
result: unknown,
|
|
5644
|
-
ctx: RawContext,
|
|
5645
|
-
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
5646
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5647
|
-
): MaybePromise<T> => {
|
|
5648
|
-
if (after === undefined) {
|
|
5649
|
-
try {
|
|
5650
|
-
return finalize(result, responseSet(ctx), ctx)
|
|
5651
|
-
} catch (err) {
|
|
5652
|
-
return logError(err, ctx, finalize, wrapResponse)
|
|
5653
|
-
}
|
|
5654
|
-
}
|
|
5655
|
-
let transformed: unknown
|
|
5656
|
-
try {
|
|
5657
|
-
transformed = after(result, ctx)
|
|
5658
|
-
} catch (err) {
|
|
5659
|
-
return logError(err, ctx, finalize, wrapResponse)
|
|
5660
|
-
}
|
|
5661
|
-
if (transformed instanceof Promise) {
|
|
5662
|
-
return transformed.then(
|
|
5663
|
-
(value) => {
|
|
5664
|
-
try {
|
|
5665
|
-
return finalize(value, responseSet(ctx), ctx)
|
|
5666
|
-
} catch (err) {
|
|
5667
|
-
return logError(err, ctx, finalize, wrapResponse)
|
|
5668
|
-
}
|
|
5669
|
-
},
|
|
5670
|
-
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
5671
|
-
)
|
|
5672
|
-
}
|
|
5673
|
-
try {
|
|
5674
|
-
return finalize(transformed, responseSet(ctx), ctx)
|
|
5675
|
-
} catch (err) {
|
|
5676
|
-
return logError(err, ctx, finalize, wrapResponse)
|
|
5835
|
+
return runDerive(ctx, route, 0)
|
|
5677
5836
|
}
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
)
|
|
5709
|
-
}
|
|
5710
|
-
return runFinish(result, ctx, finalize, wrapResponse)
|
|
5711
|
-
}
|
|
5712
|
-
|
|
5713
|
-
const runBefore = <T>(
|
|
5714
|
-
ctx: RawContext,
|
|
5715
|
-
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
5716
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5717
|
-
): MaybePromise<T> => {
|
|
5718
|
-
let early: unknown
|
|
5719
|
-
try {
|
|
5720
|
-
early = before(ctx)
|
|
5721
|
-
} catch (err) {
|
|
5722
|
-
return logError(err, ctx, finalize, wrapResponse)
|
|
5723
|
-
}
|
|
5724
|
-
if (early instanceof Promise) {
|
|
5725
|
-
return early.then(
|
|
5726
|
-
(settled) =>
|
|
5727
|
-
settled === undefined
|
|
5728
|
-
? runHandler(ctx, finalize, wrapResponse)
|
|
5729
|
-
: finalizeEarly(settled, ctx, finalize, wrapResponse),
|
|
5730
|
-
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
5731
|
-
)
|
|
5732
|
-
}
|
|
5733
|
-
if (early === undefined) return runHandler(ctx, finalize, wrapResponse)
|
|
5734
|
-
return finalizeEarly(early, ctx, finalize, wrapResponse)
|
|
5735
|
-
}
|
|
5736
|
-
|
|
5737
|
-
const runDerive = <T>(
|
|
5738
|
-
ctx: RawContext,
|
|
5739
|
-
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
5740
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5741
|
-
): MaybePromise<T> => {
|
|
5742
|
-
let derived: unknown
|
|
5743
|
-
try {
|
|
5744
|
-
derived = derive(ctx)
|
|
5745
|
-
} catch (err) {
|
|
5746
|
-
return logError(err, ctx, finalize, wrapResponse)
|
|
5747
|
-
}
|
|
5748
|
-
if (derived instanceof Promise) {
|
|
5749
|
-
return derived.then(
|
|
5750
|
-
(settled) => {
|
|
5751
|
-
if (isResponseResult(settled) || settled instanceof Response) {
|
|
5752
|
-
return finalizeEarly(settled, ctx, finalize, wrapResponse)
|
|
5753
|
-
}
|
|
5754
|
-
Object.assign(ctx, settled as object)
|
|
5755
|
-
return runBefore(ctx, finalize, wrapResponse)
|
|
5756
|
-
},
|
|
5757
|
-
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
5758
|
-
)
|
|
5759
|
-
}
|
|
5760
|
-
if (isResponseResult(derived) || derived instanceof Response) {
|
|
5761
|
-
return finalizeEarly(derived, ctx, finalize, wrapResponse)
|
|
5762
|
-
}
|
|
5763
|
-
Object.assign(ctx, derived as object)
|
|
5764
|
-
return runBefore(ctx, finalize, wrapResponse)
|
|
5765
|
-
}
|
|
5766
|
-
|
|
5767
|
-
const runValidated = <T>(
|
|
5768
|
-
validation: StandardResult<unknown> | Promise<StandardResult<unknown>>,
|
|
5769
|
-
ctx: RawContext,
|
|
5770
|
-
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
5771
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5772
|
-
): MaybePromise<T> => {
|
|
5773
|
-
if (validation instanceof Promise) {
|
|
5774
|
-
return validation.then(
|
|
5775
|
-
(settled) => {
|
|
5776
|
-
if (settled.issues !== undefined)
|
|
5777
|
-
return wrapResponse(plainValidationError(settled.issues))
|
|
5778
|
-
ctx.query = settled.value
|
|
5779
|
-
return runDerive(ctx, finalize, wrapResponse)
|
|
5780
|
-
},
|
|
5781
|
-
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
5782
|
-
)
|
|
5837
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
5838
|
+
const ctx = nativeContext
|
|
5839
|
+
? RequestContext.native(
|
|
5840
|
+
source,
|
|
5841
|
+
params,
|
|
5842
|
+
search,
|
|
5843
|
+
maxBodyBytes,
|
|
5844
|
+
platform,
|
|
5845
|
+
this.protoPoisoning,
|
|
5846
|
+
)
|
|
5847
|
+
: new RequestContext(
|
|
5848
|
+
source,
|
|
5849
|
+
params,
|
|
5850
|
+
search,
|
|
5851
|
+
signal,
|
|
5852
|
+
budget,
|
|
5853
|
+
platform,
|
|
5854
|
+
maxBodyBytes,
|
|
5855
|
+
this.protoPoisoning,
|
|
5856
|
+
)
|
|
5857
|
+
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
5858
|
+
return readBodyFramed(
|
|
5859
|
+
source,
|
|
5860
|
+
maxBodyBytes,
|
|
5861
|
+
this.protoPoisoning,
|
|
5862
|
+
(parsed) => onParsed(parsed, ctx),
|
|
5863
|
+
this.wrapWebResponse,
|
|
5864
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5865
|
+
bodySchema,
|
|
5866
|
+
this.formOrigins,
|
|
5867
|
+
) as MaybePromise<Response>
|
|
5783
5868
|
}
|
|
5784
|
-
if (validation.issues !== undefined)
|
|
5785
|
-
return wrapResponse(plainValidationError(validation.issues))
|
|
5786
|
-
ctx.query = validation.value
|
|
5787
|
-
return runDerive(ctx, finalize, wrapResponse)
|
|
5788
5869
|
}
|
|
5789
|
-
|
|
5790
|
-
return <T>(
|
|
5791
|
-
source: RequestSource,
|
|
5792
|
-
params: Record<string, string>,
|
|
5793
|
-
search: string | undefined,
|
|
5794
|
-
signal: AbortSignal,
|
|
5795
|
-
budget: RequestBudget,
|
|
5796
|
-
platform: Platform | undefined,
|
|
5797
|
-
nativeContext: boolean,
|
|
5798
|
-
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
5799
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5800
|
-
): MaybePromise<T> => {
|
|
5870
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
5801
5871
|
const ctx = nativeContext
|
|
5802
5872
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
5803
5873
|
: new RequestContext(
|
|
@@ -5810,193 +5880,383 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5810
5880
|
maxBodyBytes,
|
|
5811
5881
|
this.protoPoisoning,
|
|
5812
5882
|
)
|
|
5813
|
-
if (
|
|
5814
|
-
|
|
5883
|
+
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
5884
|
+
if (querySchema === undefined) return runDerive(ctx, route, 0)
|
|
5885
|
+
let validation: MaybePromise<StandardResult<unknown>>
|
|
5815
5886
|
try {
|
|
5816
5887
|
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]))
|
|
5817
5888
|
} catch (err) {
|
|
5818
|
-
return
|
|
5889
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
5819
5890
|
}
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
* frame running parse → validate → derive → before → handler → (after) through the caller's own
|
|
5828
|
-
* `finalize` - with the body read ahead of validation, exactly as the generic program orders its
|
|
5829
|
-
* body stage before the derive/before stages. `readBodyFramed` keeps the shared bounded-read,
|
|
5830
|
-
* content-type, and prototype-poisoning contracts; the 415/400 renders take `wrapResponse`
|
|
5831
|
-
* directly, which is what the generic lane's wrap-after-identity amounts to.
|
|
5832
|
-
*/
|
|
5833
|
-
private buildFusedBodyDeriveBeforeAfterRunner(
|
|
5834
|
-
handler: InternalHandler,
|
|
5835
|
-
derive: RawDerive,
|
|
5836
|
-
before: RawBeforeHandle,
|
|
5837
|
-
after: RawAfterHandle | undefined,
|
|
5838
|
-
bodySchema: StandardSchemaV1,
|
|
5839
|
-
maxBodyBytes: number,
|
|
5840
|
-
): FusedBodyRunner {
|
|
5841
|
-
const logError = <T>(
|
|
5842
|
-
err: unknown,
|
|
5843
|
-
ctx: RawContext,
|
|
5844
|
-
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
5845
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5846
|
-
): T => this.bareError(err, ctx, finalize, wrapResponse)
|
|
5847
|
-
|
|
5848
|
-
const runFinish = <T>(
|
|
5849
|
-
result: unknown,
|
|
5850
|
-
ctx: RawContext,
|
|
5851
|
-
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
5852
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5853
|
-
): MaybePromise<T> => {
|
|
5854
|
-
if (after === undefined) {
|
|
5855
|
-
try {
|
|
5856
|
-
return finalize(result, responseSet(ctx), ctx)
|
|
5857
|
-
} catch (err) {
|
|
5858
|
-
return logError(err, ctx, finalize, wrapResponse)
|
|
5859
|
-
}
|
|
5860
|
-
}
|
|
5861
|
-
let transformed: unknown
|
|
5862
|
-
try {
|
|
5863
|
-
transformed = after(result, ctx)
|
|
5864
|
-
} catch (err) {
|
|
5865
|
-
return logError(err, ctx, finalize, wrapResponse)
|
|
5866
|
-
}
|
|
5867
|
-
if (transformed instanceof Promise) {
|
|
5868
|
-
return transformed.then(
|
|
5869
|
-
(value) => {
|
|
5870
|
-
try {
|
|
5871
|
-
return finalize(value, responseSet(ctx), ctx)
|
|
5872
|
-
} catch (err) {
|
|
5873
|
-
return logError(err, ctx, finalize, wrapResponse)
|
|
5874
|
-
}
|
|
5891
|
+
if (validation instanceof Promise) {
|
|
5892
|
+
return validation.then(
|
|
5893
|
+
(settled) => {
|
|
5894
|
+
if (settled.issues !== undefined)
|
|
5895
|
+
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
5896
|
+
ctx.query = settled.value
|
|
5897
|
+
return runDerive(ctx, route, 0)
|
|
5875
5898
|
},
|
|
5876
|
-
(err) =>
|
|
5899
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5877
5900
|
)
|
|
5878
5901
|
}
|
|
5879
|
-
|
|
5880
|
-
return
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
}
|
|
5902
|
+
if (validation.issues !== undefined)
|
|
5903
|
+
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
5904
|
+
ctx.query = validation.value
|
|
5905
|
+
return runDerive(ctx, route, 0)
|
|
5884
5906
|
}
|
|
5907
|
+
}
|
|
5885
5908
|
|
|
5886
|
-
|
|
5909
|
+
/**
|
|
5910
|
+
* The Node-direct twin of {@link fusedWebStages}: the same chain, finalizing through the caller's
|
|
5911
|
+
* own `finalize`, shared by every route with the same hook shape. A terminal `finalize` throw (an
|
|
5912
|
+
* unstringifiable handler return on the Node lane) is caught as `finishLifecycleContract` catches it.
|
|
5913
|
+
*/
|
|
5914
|
+
private fusedNodeStages(
|
|
5915
|
+
deriveCount: HookCount,
|
|
5916
|
+
beforeCount: HookCount,
|
|
5917
|
+
afterCount: HookCount,
|
|
5918
|
+
): FusedNodeStart {
|
|
5919
|
+
const finishResult = <T>(
|
|
5887
5920
|
result: unknown,
|
|
5888
5921
|
ctx: RawContext,
|
|
5922
|
+
route: FusedHooks,
|
|
5889
5923
|
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
5890
5924
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5891
|
-
): T => {
|
|
5925
|
+
): MaybePromise<T> => {
|
|
5892
5926
|
try {
|
|
5893
5927
|
return finalize(result, responseSet(ctx), ctx)
|
|
5894
5928
|
} catch (err) {
|
|
5895
|
-
return
|
|
5929
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
5896
5930
|
}
|
|
5897
5931
|
}
|
|
5898
5932
|
|
|
5899
|
-
|
|
5933
|
+
// The stages read the route's hooks, as in the Web builder: a shape creates only the stages it uses,
|
|
5934
|
+
// a phase with one hook gets a straight-line closure, and a phase with several loops over the route's
|
|
5935
|
+
// array. The generic `T` of each stage is the caller's outcome type.
|
|
5936
|
+
type Stage = <T>(
|
|
5900
5937
|
ctx: RawContext,
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5938
|
+
route: FusedHooks,
|
|
5939
|
+
finalize: Finalize<T>,
|
|
5940
|
+
wrapResponse: Wrap<T>,
|
|
5941
|
+
index: number,
|
|
5942
|
+
) => MaybePromise<T>
|
|
5943
|
+
type AfterStage = <T>(
|
|
5944
|
+
result: unknown,
|
|
5945
|
+
ctx: RawContext,
|
|
5946
|
+
route: FusedHooks,
|
|
5947
|
+
finalize: Finalize<T>,
|
|
5948
|
+
wrapResponse: Wrap<T>,
|
|
5949
|
+
index: number,
|
|
5950
|
+
) => MaybePromise<T>
|
|
5951
|
+
|
|
5952
|
+
let runAfter: AfterStage
|
|
5953
|
+
if (afterCount === 0) {
|
|
5954
|
+
runAfter = (result, ctx, route, finalize, wrapResponse) =>
|
|
5955
|
+
finishResult(result, ctx, route, finalize, wrapResponse)
|
|
5956
|
+
} else if (afterCount === 1) {
|
|
5957
|
+
runAfter = (result, ctx, route, finalize, wrapResponse) => {
|
|
5958
|
+
let transformed: unknown
|
|
5959
|
+
try {
|
|
5960
|
+
transformed = route.afters[0]!(result, ctx)
|
|
5961
|
+
} catch (err) {
|
|
5962
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
5963
|
+
}
|
|
5964
|
+
if (transformed instanceof Promise) {
|
|
5965
|
+
return transformed.then(
|
|
5966
|
+
(value) => finishResult(value, ctx, route, finalize, wrapResponse),
|
|
5967
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
5968
|
+
)
|
|
5969
|
+
}
|
|
5970
|
+
return finishResult(transformed, ctx, route, finalize, wrapResponse)
|
|
5971
|
+
}
|
|
5972
|
+
} else {
|
|
5973
|
+
runAfter = (result, ctx, route, finalize, wrapResponse, index) => {
|
|
5974
|
+
const lastAfter = route.afters.length - 1
|
|
5975
|
+
for (;;) {
|
|
5976
|
+
let transformed: unknown
|
|
5977
|
+
try {
|
|
5978
|
+
transformed = route.afters[index]!(result, ctx)
|
|
5979
|
+
} catch (err) {
|
|
5980
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
5981
|
+
}
|
|
5982
|
+
if (transformed instanceof Promise) {
|
|
5983
|
+
return transformed.then(
|
|
5984
|
+
(value) =>
|
|
5985
|
+
index < lastAfter
|
|
5986
|
+
? runAfter(value, ctx, route, finalize, wrapResponse, index + 1)
|
|
5987
|
+
: finishResult(value, ctx, route, finalize, wrapResponse),
|
|
5988
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
5989
|
+
)
|
|
5990
|
+
}
|
|
5991
|
+
if (index === lastAfter)
|
|
5992
|
+
return finishResult(transformed, ctx, route, finalize, wrapResponse)
|
|
5993
|
+
result = transformed
|
|
5994
|
+
index++
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
5997
|
+
}
|
|
5998
|
+
|
|
5999
|
+
const runHandler: Stage = (ctx, route, finalize, wrapResponse) => {
|
|
5904
6000
|
let result: unknown
|
|
5905
6001
|
try {
|
|
5906
|
-
result = handler(ctx)
|
|
6002
|
+
result = route.handler(ctx)
|
|
5907
6003
|
} catch (err) {
|
|
5908
|
-
return
|
|
6004
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
5909
6005
|
}
|
|
5910
6006
|
if (result instanceof Promise) {
|
|
5911
6007
|
return result.then(
|
|
5912
|
-
(value) =>
|
|
5913
|
-
(err) =>
|
|
6008
|
+
(value) => runAfter(value, ctx, route, finalize, wrapResponse, 0),
|
|
6009
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
5914
6010
|
)
|
|
5915
6011
|
}
|
|
5916
|
-
return
|
|
6012
|
+
return runAfter(result, ctx, route, finalize, wrapResponse, 0)
|
|
5917
6013
|
}
|
|
5918
6014
|
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
6015
|
+
let runBefore: Stage
|
|
6016
|
+
if (beforeCount === 0) {
|
|
6017
|
+
runBefore = runHandler
|
|
6018
|
+
} else if (beforeCount === 1) {
|
|
6019
|
+
runBefore = (ctx, route, finalize, wrapResponse) => {
|
|
6020
|
+
let early: unknown
|
|
6021
|
+
try {
|
|
6022
|
+
early = route.befores[0]!(ctx)
|
|
6023
|
+
} catch (err) {
|
|
6024
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6025
|
+
}
|
|
6026
|
+
if (early instanceof Promise) {
|
|
6027
|
+
return early.then(
|
|
6028
|
+
(settled) =>
|
|
6029
|
+
settled === undefined
|
|
6030
|
+
? runHandler(ctx, route, finalize, wrapResponse, 0)
|
|
6031
|
+
: finishResult(settled, ctx, route, finalize, wrapResponse),
|
|
6032
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6033
|
+
)
|
|
6034
|
+
}
|
|
6035
|
+
if (early === undefined) return runHandler(ctx, route, finalize, wrapResponse, 0)
|
|
6036
|
+
return finishResult(early, ctx, route, finalize, wrapResponse)
|
|
5929
6037
|
}
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
6038
|
+
} else {
|
|
6039
|
+
runBefore = (ctx, route, finalize, wrapResponse, index) => {
|
|
6040
|
+
const lastBefore = route.befores.length - 1
|
|
6041
|
+
for (;;) {
|
|
6042
|
+
let early: unknown
|
|
6043
|
+
try {
|
|
6044
|
+
early = route.befores[index]!(ctx)
|
|
6045
|
+
} catch (err) {
|
|
6046
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6047
|
+
}
|
|
6048
|
+
if (early instanceof Promise) {
|
|
6049
|
+
return early.then(
|
|
6050
|
+
(settled) => {
|
|
6051
|
+
if (settled !== undefined)
|
|
6052
|
+
return finishResult(settled, ctx, route, finalize, wrapResponse)
|
|
6053
|
+
return index < lastBefore
|
|
6054
|
+
? runBefore(ctx, route, finalize, wrapResponse, index + 1)
|
|
6055
|
+
: runHandler(ctx, route, finalize, wrapResponse, 0)
|
|
6056
|
+
},
|
|
6057
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6058
|
+
)
|
|
6059
|
+
}
|
|
6060
|
+
if (early !== undefined) return finishResult(early, ctx, route, finalize, wrapResponse)
|
|
6061
|
+
if (index === lastBefore) return runHandler(ctx, route, finalize, wrapResponse, 0)
|
|
6062
|
+
index++
|
|
6063
|
+
}
|
|
5938
6064
|
}
|
|
5939
|
-
if (early === undefined) return runHandler(ctx, finalize, wrapResponse)
|
|
5940
|
-
return finalizeEarly(early, ctx, finalize, wrapResponse)
|
|
5941
6065
|
}
|
|
5942
6066
|
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
6067
|
+
let runDerive: Stage
|
|
6068
|
+
if (deriveCount === 0) {
|
|
6069
|
+
runDerive = runBefore
|
|
6070
|
+
} else if (deriveCount === 1) {
|
|
6071
|
+
runDerive = (ctx, route, finalize, wrapResponse) => {
|
|
6072
|
+
let derived: unknown
|
|
6073
|
+
try {
|
|
6074
|
+
derived = route.derives[0]!(ctx)
|
|
6075
|
+
} catch (err) {
|
|
6076
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6077
|
+
}
|
|
6078
|
+
if (derived instanceof Promise) {
|
|
6079
|
+
return derived.then(
|
|
6080
|
+
(settled) => {
|
|
6081
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
6082
|
+
return finishResult(settled, ctx, route, finalize, wrapResponse)
|
|
6083
|
+
}
|
|
6084
|
+
try {
|
|
6085
|
+
Object.assign(ctx, settled)
|
|
6086
|
+
} catch (err) {
|
|
6087
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6088
|
+
}
|
|
6089
|
+
return runBefore(ctx, route, finalize, wrapResponse, 0)
|
|
6090
|
+
},
|
|
6091
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6092
|
+
)
|
|
6093
|
+
}
|
|
6094
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
6095
|
+
return finishResult(derived, ctx, route, finalize, wrapResponse)
|
|
6096
|
+
}
|
|
6097
|
+
try {
|
|
6098
|
+
Object.assign(ctx, derived)
|
|
6099
|
+
} catch (err) {
|
|
6100
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6101
|
+
}
|
|
6102
|
+
return runBefore(ctx, route, finalize, wrapResponse, 0)
|
|
5953
6103
|
}
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
6104
|
+
} else {
|
|
6105
|
+
runDerive = (ctx, route, finalize, wrapResponse, index) => {
|
|
6106
|
+
const lastDerive = route.derives.length - 1
|
|
6107
|
+
for (;;) {
|
|
6108
|
+
let derived: unknown
|
|
6109
|
+
try {
|
|
6110
|
+
derived = route.derives[index]!(ctx)
|
|
6111
|
+
} catch (err) {
|
|
6112
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6113
|
+
}
|
|
6114
|
+
if (derived instanceof Promise) {
|
|
6115
|
+
return derived.then(
|
|
6116
|
+
(settled) => {
|
|
6117
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
6118
|
+
return finishResult(settled, ctx, route, finalize, wrapResponse)
|
|
6119
|
+
}
|
|
6120
|
+
try {
|
|
6121
|
+
Object.assign(ctx, settled)
|
|
6122
|
+
} catch (err) {
|
|
6123
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6124
|
+
}
|
|
6125
|
+
return index < lastDerive
|
|
6126
|
+
? runDerive(ctx, route, finalize, wrapResponse, index + 1)
|
|
6127
|
+
: runBefore(ctx, route, finalize, wrapResponse, 0)
|
|
6128
|
+
},
|
|
6129
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6130
|
+
)
|
|
6131
|
+
}
|
|
6132
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
6133
|
+
return finishResult(derived, ctx, route, finalize, wrapResponse)
|
|
6134
|
+
}
|
|
6135
|
+
try {
|
|
6136
|
+
Object.assign(ctx, derived)
|
|
6137
|
+
} catch (err) {
|
|
6138
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6139
|
+
}
|
|
6140
|
+
if (index === lastDerive) return runBefore(ctx, route, finalize, wrapResponse, 0)
|
|
6141
|
+
index++
|
|
6142
|
+
}
|
|
6143
|
+
}
|
|
6144
|
+
}
|
|
6145
|
+
return runDerive
|
|
6146
|
+
}
|
|
6147
|
+
|
|
6148
|
+
/**
|
|
6149
|
+
* The Node-direct twin of {@link buildFusedLifecycleWeb}: the same steps, finalizing through the
|
|
6150
|
+
* caller's own `finalize` (the Node outcome on the direct lane) instead of building a `Response`, so
|
|
6151
|
+
* the `!webFast` dispatcher runs these routes in one frame rather than the generic program. A
|
|
6152
|
+
* terminal `finalize` throw (an unstringifiable handler return on the Node lane) is caught as
|
|
6153
|
+
* `finishLifecycleContract` catches it; the body's 415/400 renders take `wrapResponse` directly.
|
|
6154
|
+
*/
|
|
6155
|
+
private buildFusedLifecycleRunner(
|
|
6156
|
+
route: FusedHooks,
|
|
6157
|
+
querySchema: StandardSchemaV1 | undefined,
|
|
6158
|
+
bodySchema: StandardSchemaV1 | undefined,
|
|
6159
|
+
maxBodyBytes: number,
|
|
6160
|
+
): FusedBodyRunner {
|
|
6161
|
+
const runDerive = sharedStages(NODE_STAGES, this, route, (d, b, a) =>
|
|
6162
|
+
this.fusedNodeStages(d, b, a),
|
|
6163
|
+
)
|
|
6164
|
+
const decorations = route.decorations
|
|
6165
|
+
|
|
6166
|
+
if (bodySchema !== undefined) {
|
|
6167
|
+
const onParsed = <T>(
|
|
6168
|
+
parsed: unknown,
|
|
6169
|
+
ctx: RawContext,
|
|
6170
|
+
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
6171
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6172
|
+
): MaybePromise<T> => {
|
|
6173
|
+
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
6174
|
+
try {
|
|
6175
|
+
validation = bodySchema["~standard"].validate(parsed)
|
|
6176
|
+
} catch (err) {
|
|
6177
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6178
|
+
}
|
|
6179
|
+
if (validation instanceof Promise) {
|
|
6180
|
+
return validation.then(
|
|
6181
|
+
(settled) => {
|
|
6182
|
+
if (settled.issues !== undefined)
|
|
6183
|
+
return wrapResponse(plainValidationError(settled.issues))
|
|
6184
|
+
ctx.body = settled.value
|
|
6185
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0)
|
|
6186
|
+
},
|
|
6187
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6188
|
+
)
|
|
6189
|
+
}
|
|
6190
|
+
if (validation.issues !== undefined)
|
|
6191
|
+
return wrapResponse(plainValidationError(validation.issues))
|
|
6192
|
+
ctx.body = validation.value
|
|
6193
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0)
|
|
5965
6194
|
}
|
|
5966
|
-
|
|
5967
|
-
|
|
6195
|
+
return <T>(
|
|
6196
|
+
source: RequestSource,
|
|
6197
|
+
params: Record<string, string>,
|
|
6198
|
+
search: string | undefined,
|
|
6199
|
+
signal: AbortSignal,
|
|
6200
|
+
budget: RequestBudget,
|
|
6201
|
+
platform: Platform | undefined,
|
|
6202
|
+
nativeContext: boolean,
|
|
6203
|
+
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
6204
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6205
|
+
): MaybePromise<T> => {
|
|
6206
|
+
const ctx = nativeContext
|
|
6207
|
+
? RequestContext.native(
|
|
6208
|
+
source,
|
|
6209
|
+
params,
|
|
6210
|
+
search,
|
|
6211
|
+
maxBodyBytes,
|
|
6212
|
+
platform,
|
|
6213
|
+
this.protoPoisoning,
|
|
6214
|
+
)
|
|
6215
|
+
: new RequestContext(
|
|
6216
|
+
source,
|
|
6217
|
+
params,
|
|
6218
|
+
search,
|
|
6219
|
+
signal,
|
|
6220
|
+
budget,
|
|
6221
|
+
platform,
|
|
6222
|
+
maxBodyBytes,
|
|
6223
|
+
this.protoPoisoning,
|
|
6224
|
+
)
|
|
6225
|
+
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
6226
|
+
return readBodyFramed(
|
|
6227
|
+
source,
|
|
6228
|
+
maxBodyBytes,
|
|
6229
|
+
this.protoPoisoning,
|
|
6230
|
+
(parsed) => onParsed(parsed, ctx, finalize, wrapResponse),
|
|
6231
|
+
wrapResponse,
|
|
6232
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6233
|
+
bodySchema,
|
|
6234
|
+
this.formOrigins,
|
|
6235
|
+
)
|
|
5968
6236
|
}
|
|
5969
|
-
Object.assign(ctx, derived as object)
|
|
5970
|
-
return runBefore(ctx, finalize, wrapResponse)
|
|
5971
6237
|
}
|
|
5972
6238
|
|
|
5973
|
-
const
|
|
5974
|
-
|
|
6239
|
+
const runValidated = <T>(
|
|
6240
|
+
validation: StandardResult<unknown> | Promise<StandardResult<unknown>>,
|
|
5975
6241
|
ctx: RawContext,
|
|
5976
6242
|
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
5977
6243
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5978
6244
|
): MaybePromise<T> => {
|
|
5979
|
-
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
5980
|
-
try {
|
|
5981
|
-
validation = bodySchema["~standard"].validate(parsed)
|
|
5982
|
-
} catch (err) {
|
|
5983
|
-
return logError(err, ctx, finalize, wrapResponse)
|
|
5984
|
-
}
|
|
5985
6245
|
if (validation instanceof Promise) {
|
|
5986
6246
|
return validation.then(
|
|
5987
6247
|
(settled) => {
|
|
5988
6248
|
if (settled.issues !== undefined)
|
|
5989
6249
|
return wrapResponse(plainValidationError(settled.issues))
|
|
5990
|
-
ctx.
|
|
5991
|
-
return runDerive(ctx, finalize, wrapResponse)
|
|
6250
|
+
ctx.query = settled.value
|
|
6251
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0)
|
|
5992
6252
|
},
|
|
5993
|
-
(err) =>
|
|
6253
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
5994
6254
|
)
|
|
5995
6255
|
}
|
|
5996
6256
|
if (validation.issues !== undefined)
|
|
5997
6257
|
return wrapResponse(plainValidationError(validation.issues))
|
|
5998
|
-
ctx.
|
|
5999
|
-
return runDerive(ctx, finalize, wrapResponse)
|
|
6258
|
+
ctx.query = validation.value
|
|
6259
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0)
|
|
6000
6260
|
}
|
|
6001
6261
|
|
|
6002
6262
|
return <T>(
|
|
@@ -6022,16 +6282,73 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6022
6282
|
maxBodyBytes,
|
|
6023
6283
|
this.protoPoisoning,
|
|
6024
6284
|
)
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6285
|
+
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
6286
|
+
if (querySchema === undefined) return runDerive(ctx, route, finalize, wrapResponse, 0)
|
|
6287
|
+
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
6288
|
+
try {
|
|
6289
|
+
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]))
|
|
6290
|
+
} catch (err) {
|
|
6291
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6292
|
+
}
|
|
6293
|
+
return runValidated(validation, ctx, finalize, wrapResponse)
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
|
|
6297
|
+
/** The thrown-value render of the Web fused lifecycle lanes for a route with no error hooks: a
|
|
6298
|
+
* `Response` is control flow, anything else is logged and answered with a plain 500. */
|
|
6299
|
+
private fusedWebFail(err: unknown, ctx: RawContext): Response {
|
|
6300
|
+
return this.bareError(err, ctx, this.finalizeResponse, this.wrapWebResponse)
|
|
6301
|
+
}
|
|
6302
|
+
|
|
6303
|
+
/** {@link fusedWebFail} for a fused route that may have error hooks. */
|
|
6304
|
+
private fusedWebRouteFail(
|
|
6305
|
+
err: unknown,
|
|
6306
|
+
ctx: RawContext,
|
|
6307
|
+
route: FusedHooks,
|
|
6308
|
+
): MaybePromise<Response> {
|
|
6309
|
+
return this.fusedFail(err, ctx, route, this.finalizeResponse, this.wrapWebResponse)
|
|
6310
|
+
}
|
|
6311
|
+
|
|
6312
|
+
/** The thrown-value render of a fused lifecycle route. A `Response` or `status(...)` is control flow
|
|
6313
|
+
* that no error hook sees, unless it cannot render, which is a fault like any other. Anything else
|
|
6314
|
+
* goes to the route's error hooks; with none it is logged and answered with a plain 500. */
|
|
6315
|
+
private fusedFail<T>(
|
|
6316
|
+
err: unknown,
|
|
6317
|
+
ctx: RawContext,
|
|
6318
|
+
route: FusedHooks,
|
|
6319
|
+
finalize: Finalize<T>,
|
|
6320
|
+
wrapResponse: Wrap<T>,
|
|
6321
|
+
): MaybePromise<T> {
|
|
6322
|
+
if (route.errors.length === 0) return this.bareError(err, ctx, finalize, wrapResponse)
|
|
6323
|
+
let fault = err
|
|
6324
|
+
if (err instanceof Response || isResponseResult(err)) {
|
|
6325
|
+
try {
|
|
6326
|
+
return finalize(err, responseSet(ctx), ctx)
|
|
6327
|
+
} catch (renderError) {
|
|
6328
|
+
fault = renderError
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
return this.runErrorHooks(
|
|
6332
|
+
route.errors,
|
|
6333
|
+
undefined,
|
|
6334
|
+
fault,
|
|
6335
|
+
ctx,
|
|
6336
|
+
(result, set) => finalize(result, set, ctx),
|
|
6337
|
+
wrapResponse,
|
|
6338
|
+
)
|
|
6339
|
+
}
|
|
6340
|
+
|
|
6341
|
+
/** {@link respondFused} for a fused lifecycle route, whose failures reach its error hooks. */
|
|
6342
|
+
private respondHooks(
|
|
6343
|
+
result: unknown,
|
|
6344
|
+
ctx: RawContext,
|
|
6345
|
+
route: FusedHooks,
|
|
6346
|
+
unstamped?: boolean,
|
|
6347
|
+
): MaybePromise<Response> {
|
|
6348
|
+
try {
|
|
6349
|
+
return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, unstamped)
|
|
6350
|
+
} catch (err) {
|
|
6351
|
+
return this.fusedWebRouteFail(err, ctx, route)
|
|
6035
6352
|
}
|
|
6036
6353
|
}
|
|
6037
6354
|
|
|
@@ -6818,17 +7135,21 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6818
7135
|
* semantics remain in the plan; this closure only supplies native params and deadline fallback. */
|
|
6819
7136
|
private compileBunNativeHandler(
|
|
6820
7137
|
entry: RouteEntry,
|
|
7138
|
+
method: string,
|
|
6821
7139
|
paramNames: readonly string[],
|
|
6822
7140
|
fused: FusedWebRunner | undefined,
|
|
6823
7141
|
signal: AbortSignal | undefined,
|
|
6824
7142
|
budget: RequestBudget | undefined,
|
|
6825
7143
|
peer: PeerPlatform,
|
|
6826
7144
|
fallback: BunNativeHandler,
|
|
7145
|
+
hookedPlatform?: (request: Request, server: BunPeerServer) => Platform<EnvOf<Ctx>>,
|
|
6827
7146
|
): BunNativeHandler {
|
|
6828
7147
|
// This callback is reached only from Bun's compiled native route table. Bun has already parsed
|
|
6829
7148
|
// the HTTP framing, so the JSON lane may retain its native fused `json()` parse without weakening
|
|
6830
|
-
// the portable/adapted source contract (those sources are never marked).
|
|
6831
|
-
|
|
7149
|
+
// the portable/adapted source contract (those sources are never marked). Bun hands a GET or
|
|
7150
|
+
// HEAD slot no body, so it skips the symbol write on every request.
|
|
7151
|
+
const markFramed =
|
|
7152
|
+
method === "GET" || method === "HEAD" ? (): void => undefined : markTrustedBodyFraming
|
|
6832
7153
|
// The direct native lanes bypass `runMatched`, so the route's transport byte cap must be
|
|
6833
7154
|
// marked here too - otherwise a Bun `listen()` fused route would leave direct `c.req` body
|
|
6834
7155
|
// reads uncapped. The fallback branches go through `fetchMatched` -> `runMatched`, which marks.
|
|
@@ -6857,8 +7178,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6857
7178
|
this.wrapWebResponse,
|
|
6858
7179
|
)
|
|
6859
7180
|
: undefined)
|
|
7181
|
+
// The cap is never marked on a GET or HEAD, the only methods a GET or HEAD slot is handed, so those
|
|
7182
|
+
// slots skip the wrapper and its read of the method on every request.
|
|
6860
7183
|
const capped: FusedWebRunner | undefined =
|
|
6861
|
-
inner === undefined || bodyLimit === undefined
|
|
7184
|
+
inner === undefined || bodyLimit === undefined || method === "GET" || method === "HEAD"
|
|
6862
7185
|
? inner
|
|
6863
7186
|
: (source, params, search, fusedSignal, fusedBudget, platform, nativeContext) => {
|
|
6864
7187
|
const method = source.method
|
|
@@ -6895,6 +7218,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6895
7218
|
: finishNative(request, outcome)
|
|
6896
7219
|
}
|
|
6897
7220
|
if (paramNames.length === 0) {
|
|
7221
|
+
if (hookedPlatform !== undefined) {
|
|
7222
|
+
return (request, server) => {
|
|
7223
|
+
markFramed(request)
|
|
7224
|
+
return this.fetchMatchedHooked(
|
|
7225
|
+
request,
|
|
7226
|
+
entry,
|
|
7227
|
+
EMPTY_PARAMS,
|
|
7228
|
+
hookedPlatform(request, server),
|
|
7229
|
+
)
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
6898
7232
|
if (capped === undefined) {
|
|
6899
7233
|
return (request) => {
|
|
6900
7234
|
markFramed(request)
|
|
@@ -6919,6 +7253,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6919
7253
|
paramNames.length === 1
|
|
6920
7254
|
? (params: Record<string, string>) => unroutedParam(params[paramNames[0]!])
|
|
6921
7255
|
: hasUnroutedParam
|
|
7256
|
+
if (hookedPlatform !== undefined) {
|
|
7257
|
+
return (request, server) => {
|
|
7258
|
+
markFramed(request)
|
|
7259
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Bun's route table hands each handler a Request carrying the `params` it matched.
|
|
7260
|
+
const params = (request as BunRequestWithParams).params ?? EMPTY_PARAMS
|
|
7261
|
+
if (malformed(params)) return fallback(request, server)
|
|
7262
|
+
return this.fetchMatchedHooked(request, entry, params, hookedPlatform(request, server))
|
|
7263
|
+
}
|
|
7264
|
+
}
|
|
6922
7265
|
if (capped === undefined) {
|
|
6923
7266
|
return (request, server) => {
|
|
6924
7267
|
markFramed(request)
|
|
@@ -6945,9 +7288,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6945
7288
|
}
|
|
6946
7289
|
}
|
|
6947
7290
|
|
|
6948
|
-
/** Compile portable route registrations into Bun's native route table.
|
|
6949
|
-
*
|
|
6950
|
-
* route
|
|
7291
|
+
/** Compile portable route registrations into Bun's native route table. Paths a composed mount or
|
|
7292
|
+
* a WebSocket handshake may reach stay with the portable dispatcher; provably disjoint HTTP routes
|
|
7293
|
+
* keep native matching. Request hooks run before the matched route and rewrites return to dispatch.
|
|
6951
7294
|
*
|
|
6952
7295
|
* Bun and the portable router agree on which of two static-and-`:param` paths wins a request, so
|
|
6953
7296
|
* the table is kept to those paths. Where Bun would otherwise pass a request on to a less specific
|
|
@@ -6956,21 +7299,21 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6956
7299
|
private buildBunNativeRoutes(
|
|
6957
7300
|
fallback: BunNativeHandler,
|
|
6958
7301
|
peer: PeerPlatform,
|
|
7302
|
+
platformFor?: (request: Request, server: BunPeerServer) => Platform<EnvOf<Ctx>>,
|
|
6959
7303
|
): BunNativeRoutes | undefined {
|
|
6960
|
-
|
|
6961
|
-
// bypasses - so an app that declares trust routes through the fetch lane (where `c.clientIp`
|
|
6962
|
-
// resolves) instead of Bun's native table. The allocation-free default keeps native fusion.
|
|
7304
|
+
const hooked = this.onRequestHooks.length > 0 && !this.bunNativeRequestHooksSafe
|
|
6963
7305
|
if (
|
|
6964
|
-
(
|
|
6965
|
-
|
|
6966
|
-
this.
|
|
6967
|
-
this.
|
|
7306
|
+
(hooked && platformFor === undefined) ||
|
|
7307
|
+
// A hook may rewrite a handshake onto a socket path, which only the dispatcher's upgrade check follows.
|
|
7308
|
+
(hooked && this.wsRoutes.length > 0) ||
|
|
7309
|
+
this.mountsTakeUpgrades()
|
|
6968
7310
|
) {
|
|
6969
7311
|
return undefined
|
|
6970
7312
|
}
|
|
6971
7313
|
|
|
6972
7314
|
const routes: BunNativeRoutes = Object.create(null) as BunNativeRoutes
|
|
6973
7315
|
const mayUseFusedNative =
|
|
7316
|
+
!hooked &&
|
|
6974
7317
|
this.requestTimeoutMs === 0 &&
|
|
6975
7318
|
(this.onResponseHooks.length === 0 || this.bunNativeResponseHeadersOnly) &&
|
|
6976
7319
|
this.onResponseFinalizedHooks.length === 0 &&
|
|
@@ -6981,29 +7324,50 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6981
7324
|
const unboundedBudget = mayUseFusedNative ? getUnboundedRequestBudget() : undefined
|
|
6982
7325
|
let count = 0
|
|
6983
7326
|
const all = this.catalog.entries()
|
|
7327
|
+
const preRouteMounts = this.fetchMounts.filter((mount) => mount.beforeRoutes)
|
|
6984
7328
|
// A path with a wildcard or a part-literal segment cannot go in the table: Bun reads `/:name.json`
|
|
6985
7329
|
// as one parameter named `name.json`. These are filed under each run of static segments they
|
|
6986
7330
|
// start with, which is where a table path they could outrank looks for them.
|
|
6987
|
-
const under = new Map<string, CatalogRoute[]>()
|
|
7331
|
+
const under = new Map<string, CatalogRoute["pattern"]["segments"][]>()
|
|
7332
|
+
const fileUnder = (segments: CatalogRoute["pattern"]["segments"]): void => {
|
|
7333
|
+
let prefix = ""
|
|
7334
|
+
for (const segment of segments) {
|
|
7335
|
+
let bucket = under.get(prefix)
|
|
7336
|
+
if (bucket === undefined) {
|
|
7337
|
+
bucket = []
|
|
7338
|
+
under.set(prefix, bucket)
|
|
7339
|
+
}
|
|
7340
|
+
bucket.push(segments)
|
|
7341
|
+
if (segment.kind !== "static") break
|
|
7342
|
+
prefix += `/${segment.value}`
|
|
7343
|
+
}
|
|
7344
|
+
}
|
|
7345
|
+
// A handshake is a GET the portable dispatcher gives to a socket route before any HTTP route, so a
|
|
7346
|
+
// socket path is outside the table like a wildcard path, and a GET route a request may share with
|
|
7347
|
+
// one stays with the dispatcher.
|
|
7348
|
+
const sockets = this.wsRoutes.map((route) => compileRoutePattern(route.path).segments)
|
|
7349
|
+
for (const segments of sockets) fileUnder(segments)
|
|
6988
7350
|
const lead = all.map((route) => {
|
|
6989
7351
|
const segments = route.pattern.segments
|
|
6990
7352
|
const plain = segments.every(
|
|
6991
7353
|
(segment) => segment.kind === "static" || segment.kind === "param",
|
|
6992
7354
|
)
|
|
7355
|
+
if (!plain) fileUnder(segments)
|
|
6993
7356
|
let prefix = ""
|
|
6994
7357
|
for (const segment of segments) {
|
|
6995
|
-
if (!plain) {
|
|
6996
|
-
let bucket = under.get(prefix)
|
|
6997
|
-
if (bucket === undefined) {
|
|
6998
|
-
bucket = []
|
|
6999
|
-
under.set(prefix, bucket)
|
|
7000
|
-
}
|
|
7001
|
-
bucket.push(route)
|
|
7002
|
-
}
|
|
7003
7358
|
if (segment.kind !== "static") break
|
|
7004
7359
|
prefix += `/${segment.value}`
|
|
7005
7360
|
}
|
|
7006
|
-
|
|
7361
|
+
// Mounts take every method under their literal prefix before route matching. A parameter may
|
|
7362
|
+
// fill the rest of that prefix, but a static route ending above the mount cannot reach it.
|
|
7363
|
+
// Conservative by design: only the leading static run proves a dynamic route disjoint.
|
|
7364
|
+
const mayHitMount = preRouteMounts.some(
|
|
7365
|
+
(mount) =>
|
|
7366
|
+
underMountPrefix(prefix, mount.path) ||
|
|
7367
|
+
(route.pattern.paramNames.length > 0 &&
|
|
7368
|
+
(prefix === "" || underMountPrefix(mount.path, prefix))),
|
|
7369
|
+
)
|
|
7370
|
+
return plain && !mayHitMount ? prefix : undefined
|
|
7007
7371
|
})
|
|
7008
7372
|
// A preflight hook is intentionally handled by the fallback fetch path. Keeping OPTIONS out
|
|
7009
7373
|
// of the native lane means Bun dispatches it through CORS's onRequest hook even when the
|
|
@@ -7033,7 +7397,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7033
7397
|
entry.idempotent !== undefined ||
|
|
7034
7398
|
!BUN_TABLE_METHODS.includes(method as Method) ||
|
|
7035
7399
|
(preflightOnFallback && method === "OPTIONS") ||
|
|
7036
|
-
under.get(run)?.some((other) => outranks(other
|
|
7400
|
+
under.get(run)?.some((other) => outranks(other, pattern.segments)) ||
|
|
7401
|
+
(method === "GET" && sockets.some((socket) => mayShareRequest(socket, pattern.segments)))
|
|
7037
7402
|
) {
|
|
7038
7403
|
continue
|
|
7039
7404
|
}
|
|
@@ -7041,16 +7406,24 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7041
7406
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined
|
|
7042
7407
|
const handler = this.compileBunNativeHandler(
|
|
7043
7408
|
entry,
|
|
7409
|
+
method,
|
|
7044
7410
|
paramNames,
|
|
7045
7411
|
fused,
|
|
7046
7412
|
unboundedSignal,
|
|
7047
7413
|
unboundedBudget,
|
|
7048
7414
|
peer,
|
|
7049
7415
|
fallback,
|
|
7416
|
+
hooked ? platformFor : undefined,
|
|
7050
7417
|
)
|
|
7051
|
-
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it.
|
|
7052
|
-
|
|
7053
|
-
|
|
7418
|
+
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it. Left
|
|
7419
|
+
// off, `validateHost` costs nothing here: the table holds no wildcard, a `..` parameter falls
|
|
7420
|
+
// back, and reading the Host on every request measured about 6% of a bare route.
|
|
7421
|
+
tableAt(path, pattern.segments)[method as Method] = this.validateHost
|
|
7422
|
+
? (request, server) =>
|
|
7423
|
+
requestTarget(request, true, this.normalizeTarget)
|
|
7424
|
+
? fallback(request, server)
|
|
7425
|
+
: handler(request, server)
|
|
7426
|
+
: handler
|
|
7054
7427
|
count += 1
|
|
7055
7428
|
if (paramNames.length > 0) {
|
|
7056
7429
|
below.add(pattern.segments.length + method)
|
|
@@ -7101,11 +7474,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7101
7474
|
* nothing - so any endpoint slower than 10s (a render, an export, a long upstream call) has its
|
|
7102
7475
|
* connection cut mid-flight regardless of `requestTimeoutMs`. Apps with such endpoints must raise
|
|
7103
7476
|
* this above their slowest expected response, which is why it is a first-class option and not a
|
|
7104
|
-
* reason to drop down to `Bun.serve
|
|
7477
|
+
* reason to drop down to `Bun.serve`, or hold those requests' connections alone with
|
|
7478
|
+
* `withIdleTimeout()` from `@nifrajs/core/idle-timeout`. `0` disables the timeout entirely; max 255.
|
|
7105
7479
|
*
|
|
7106
7480
|
* `tls` serves HTTPS directly: `app.listen(443, { tls: { cert, key } })` with PEM text or bytes.
|
|
7107
7481
|
* Requests then arrive with `https:` URLs. Behind a proxy or a platform that terminates TLS, leave
|
|
7108
7482
|
* it unset.
|
|
7483
|
+
*
|
|
7484
|
+
* `static` sees every request before the app does, the ones Bun's route table serves included, so
|
|
7485
|
+
* a file it answers wins over any route at the same path. It returns the file's response, or
|
|
7486
|
+
* `undefined` to let the app route the request. No hook of the app runs for a request it answers.
|
|
7487
|
+
* `@nifrajs/web`'s Bun build serves its client bundle and public files this way.
|
|
7109
7488
|
*/
|
|
7110
7489
|
listen(
|
|
7111
7490
|
port: number,
|
|
@@ -7114,6 +7493,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7114
7493
|
readonly hostname?: string
|
|
7115
7494
|
readonly idleTimeoutSec?: number
|
|
7116
7495
|
readonly tls?: ListenTlsOptions
|
|
7496
|
+
readonly static?: (request: Request) => MaybePromise<Response | undefined>
|
|
7117
7497
|
},
|
|
7118
7498
|
): RunningServer {
|
|
7119
7499
|
if (typeof Bun === "undefined") {
|
|
@@ -7177,6 +7557,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7177
7557
|
return jsonError(500, "internal_error")
|
|
7178
7558
|
},
|
|
7179
7559
|
}
|
|
7560
|
+
const idleTimeoutSec = options?.idleTimeoutSec ?? BUN_IDLE_TIMEOUT_SEC
|
|
7561
|
+
const setIdleTimeout: IdleTimeoutSetter = (request, seconds) =>
|
|
7562
|
+
// biome-ignore lint/plugin/noChainedTypeAssertions lint/plugin/requireSafetyCommentForTypeAssertion: `running` is the Bun `Server` this call started (`RunningServer` publishes a subset of it), and every source a Bun listener routes is a `Request`.
|
|
7563
|
+
(running as unknown as BunPeerServer).timeout(request as Request, seconds ?? idleTimeoutSec)
|
|
7180
7564
|
const fallback: BunNativeHandler = (req, server) => {
|
|
7181
7565
|
// Bun has already framed and bounded this request body in its HTTP parser. Mark the
|
|
7182
7566
|
// source before the portable fallback runs so body schemas use Bun's native `json()`
|
|
@@ -7184,27 +7568,73 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7184
7568
|
// Requests. Native route handlers mark themselves in compileBunNativeHandler; this
|
|
7185
7569
|
// covers every request the native lane does not serve.
|
|
7186
7570
|
markTrustedBodyFraming(req)
|
|
7187
|
-
return this.fetch(
|
|
7571
|
+
return this.fetch(
|
|
7572
|
+
req,
|
|
7573
|
+
new BunPeerPlatform(server, req, setIdleTimeout) as Platform<EnvOf<Ctx>>,
|
|
7574
|
+
)
|
|
7188
7575
|
}
|
|
7189
7576
|
// Bun names the socket peer of a request when its server is asked. A request served from the
|
|
7190
7577
|
// native table carries this one platform, which asks only when a handler reads `c.clientIp`.
|
|
7578
|
+
const socketPeer = (request: RequestSource): string | undefined =>
|
|
7579
|
+
(running as unknown as BunPeerServer).requestIP(request as Request)?.address
|
|
7580
|
+
// A `clientIp` trust declaration resolves here too, from the request's headers, when a handler reads
|
|
7581
|
+
// the address; only a declaration of zero hops names the socket peer.
|
|
7582
|
+
const trust = this.clientIpTrust
|
|
7191
7583
|
const peer: PeerPlatform = {
|
|
7192
|
-
[PLATFORM_PEER]:
|
|
7193
|
-
|
|
7584
|
+
[PLATFORM_PEER]:
|
|
7585
|
+
trust === undefined
|
|
7586
|
+
? socketPeer
|
|
7587
|
+
: (request) =>
|
|
7588
|
+
resolveClientIp(
|
|
7589
|
+
!("header" in trust) && trust.trustedHops === 0 ? socketPeer(request) : undefined,
|
|
7590
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: every source a Bun listener routes is a `Request`.
|
|
7591
|
+
request as Request,
|
|
7592
|
+
trust,
|
|
7593
|
+
),
|
|
7594
|
+
[PLATFORM_IDLE_TIMEOUT]: setIdleTimeout,
|
|
7595
|
+
}
|
|
7596
|
+
const nativeRoutes = this.buildBunNativeRoutes(
|
|
7597
|
+
fallback,
|
|
7598
|
+
peer,
|
|
7599
|
+
(request, server) =>
|
|
7600
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: `fallback` hands `fetch` the same platform, which holds no environment.
|
|
7601
|
+
new BunPeerPlatform(server, request, setIdleTimeout) as Platform<EnvOf<Ctx>>,
|
|
7602
|
+
)
|
|
7603
|
+
const first = options?.static
|
|
7604
|
+
const before = <S, A>(
|
|
7605
|
+
inner: (req: Request, server: S) => MaybePromise<A>,
|
|
7606
|
+
): ((req: Request, server: S) => MaybePromise<A | Response>) =>
|
|
7607
|
+
first === undefined
|
|
7608
|
+
? inner
|
|
7609
|
+
: (req, server) => {
|
|
7610
|
+
const answer = first(req)
|
|
7611
|
+
if (answer === undefined) return inner(req, server)
|
|
7612
|
+
return answer instanceof Promise
|
|
7613
|
+
? answer.then((response) => response ?? inner(req, server))
|
|
7614
|
+
: answer
|
|
7615
|
+
}
|
|
7616
|
+
// Bun runs a table handler without `fetch`, so each one gets the same front as the fallback.
|
|
7617
|
+
if (first !== undefined && nativeRoutes !== undefined) {
|
|
7618
|
+
for (const path in nativeRoutes) {
|
|
7619
|
+
const methods = nativeRoutes[path]!
|
|
7620
|
+
for (const method of BUN_TABLE_METHODS) {
|
|
7621
|
+
const handler = methods[method]
|
|
7622
|
+
if (handler !== undefined) methods[method] = before(handler)
|
|
7623
|
+
}
|
|
7624
|
+
}
|
|
7194
7625
|
}
|
|
7195
|
-
const nativeRoutes =
|
|
7196
|
-
wsHandlers === undefined ? this.buildBunNativeRoutes(fallback, peer) : undefined
|
|
7197
7626
|
const sockets = new Set<{ close(code?: number): void }>()
|
|
7198
7627
|
this.bunSockets = sockets
|
|
7199
7628
|
const running = (wsHandlers === undefined
|
|
7200
7629
|
? Bun.serve({
|
|
7201
7630
|
...listening,
|
|
7202
7631
|
...(nativeRoutes === undefined ? {} : { routes: nativeRoutes }),
|
|
7203
|
-
fetch: fallback,
|
|
7632
|
+
fetch: before(fallback),
|
|
7204
7633
|
})
|
|
7205
7634
|
: Bun.serve<BunWsData>({
|
|
7206
7635
|
...listening,
|
|
7207
|
-
|
|
7636
|
+
...(nativeRoutes === undefined ? {} : { routes: nativeRoutes }),
|
|
7637
|
+
fetch: before((req, server) => this.bunFetchWithWebSocket(req, server, setIdleTimeout)),
|
|
7208
7638
|
// Bun's `ServerWebSocket<BunWsData>` is runtime-compatible with the handlers' structural
|
|
7209
7639
|
// `BunSocket` view (kept local so `Bun.*` types never leak into the published .d.ts); the
|
|
7210
7640
|
// `unknown` params bridge a TS structural-variance quirk. Round-trip covered by websocket.test.ts.
|