@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/dist/server/server.js
CHANGED
|
@@ -8,7 +8,7 @@ import { adoptRouteRuntimes, compileRouteOptions } from "../internal/route-compi
|
|
|
8
8
|
import { compileRouteExecutionPlan, } from "../internal/route-execution.js";
|
|
9
9
|
import { authenticateMount, compileRouteProgram, executeRouteProgram, } from "../internal/route-program.js";
|
|
10
10
|
import { isSameOriginRequest } from "../internal/same-origin.js";
|
|
11
|
-
import { decodeRouteParams, expandOptionalParams, } from "../router/pattern.js";
|
|
11
|
+
import { compileRoutePattern, decodeRouteParams, expandOptionalParams, } from "../router/pattern.js";
|
|
12
12
|
import { EMPTY_PARAMS, METHODS, Router } from "../router/router.js";
|
|
13
13
|
import { withAnswerSteps } from "./answer-step.js";
|
|
14
14
|
import { isAuthenticatedRequest, markAuthenticatedRequest, } from "./auth.js";
|
|
@@ -22,8 +22,9 @@ import { declaredHeaders, headerObjectOf } from "./headers.js";
|
|
|
22
22
|
import { isRoutableMethod, jsonError, pathnameOf, plainError, urlPartsOf, } from "./http.js";
|
|
23
23
|
import { withStaticNodeHeaders } from "./node-outcome.js";
|
|
24
24
|
import { queryObjectOf, searchOf } from "./query.js";
|
|
25
|
-
import { PLATFORM_PEER, RequestContext, readBodyFramed, } from "./request-context.js";
|
|
25
|
+
import { PLATFORM_IDLE_TIMEOUT, PLATFORM_PEER, PLATFORM_REQUEST, RequestContext, readBodyFramed, } from "./request-context.js";
|
|
26
26
|
import { recordRequestReplacement } from "./request-lineage.js";
|
|
27
|
+
import { carryToCopy } from "./request-local.js";
|
|
27
28
|
import { needsResolving, resolveDotSegments } from "./request-target.js";
|
|
28
29
|
import { applyStaticResponseHeaders, buildStaticResponseHeaders, claimHandedOver, fusedRespond, fusedRespondNoSet, handedBackResponse, recordResidentTexts, toResponse, writableResponse, } from "./respond.js";
|
|
29
30
|
import { CONTEXT_REQUEST, CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, } from "./runtime-core.js";
|
|
@@ -39,24 +40,32 @@ import { jsonLogger } from "./logger.js";
|
|
|
39
40
|
const BUN_NATIVE_REQUEST_SAFE = Symbol.for("@nifrajs/core/bun-native-request-safe");
|
|
40
41
|
const functionToString = Function.prototype.toString;
|
|
41
42
|
const CONTEXTLESS_ARROW = /^(?:async\s*)?\(\s*\)\s*(?::[\s\S]*?)?=>/;
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The socket peer Bun observed (`undefined` if unknown) and its connection's idle timeout, as a
|
|
45
|
+
* `Platform` for one request. A class, not an object literal: JSC builds a literal holding a getter
|
|
46
|
+
* or a symbol key on a slow path, measured at about 5x the instructions of this instance.
|
|
47
|
+
*/
|
|
48
|
+
class BunPeerPlatform {
|
|
49
|
+
#server;
|
|
50
|
+
#resolved = false;
|
|
51
|
+
#address;
|
|
52
|
+
constructor(server, req, setIdleTimeout) {
|
|
53
|
+
this.#server = server;
|
|
54
|
+
this[PLATFORM_IDLE_TIMEOUT] = setIdleTimeout;
|
|
55
|
+
this[PLATFORM_REQUEST] = req;
|
|
56
|
+
}
|
|
44
57
|
// Bun's requestIP() costs ~0.5 us per call, about 7% of a bare GET's server time. Keep the
|
|
45
58
|
// documented raw-peer c.clientIp behavior, but resolve it lazily: most routes never read c.clientIp,
|
|
46
59
|
// so they should not pay for the socket lookup on every request. A getter also
|
|
47
60
|
// preserves middleware that inspects the platform argument directly and trust-mode routes, which
|
|
48
61
|
// resolve the value in deriveClientIp before the handler runs.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
return address;
|
|
58
|
-
},
|
|
59
|
-
};
|
|
62
|
+
get clientIp() {
|
|
63
|
+
if (!this.#resolved) {
|
|
64
|
+
this.#resolved = true;
|
|
65
|
+
this.#address = this.#server.requestIP(this[PLATFORM_REQUEST])?.address;
|
|
66
|
+
}
|
|
67
|
+
return this.#address;
|
|
68
|
+
}
|
|
60
69
|
}
|
|
61
70
|
// Bun's route table has a slot for each of these; a QUERY entry is ignored and the request falls
|
|
62
71
|
// through to the table's fetch, so QUERY routes stay on the portable dispatcher.
|
|
@@ -87,6 +96,20 @@ function outranks(other, route) {
|
|
|
87
96
|
}
|
|
88
97
|
return ahead && other.length === route.length;
|
|
89
98
|
}
|
|
99
|
+
/** Whether one request can match both patterns: no two static segments differ before either ends
|
|
100
|
+
* in a wildcard, and the lengths agree otherwise. Errs toward `true`. */
|
|
101
|
+
function mayShareRequest(a, b) {
|
|
102
|
+
const shared = Math.min(a.length, b.length);
|
|
103
|
+
for (let i = 0; i < shared; i++) {
|
|
104
|
+
const left = a[i];
|
|
105
|
+
const right = b[i];
|
|
106
|
+
if (left.kind === "wildcard" || right.kind === "wildcard")
|
|
107
|
+
return true;
|
|
108
|
+
if (left.kind === "static" && right.kind === "static" && left.value !== right.value)
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return a.length === b.length;
|
|
112
|
+
}
|
|
90
113
|
/**
|
|
91
114
|
* The key Bun's route table holds `path` under. Bun refuses one with a character outside printable
|
|
92
115
|
* ASCII, so such a path is spelled as the portable router matches it: percent-encoded static
|
|
@@ -138,6 +161,8 @@ const BODY_ERROR = (error) => Promise.reject(error);
|
|
|
138
161
|
const DEFAULT_DRAIN_MS = 10_000;
|
|
139
162
|
const DRAIN_POLL_MS = 10;
|
|
140
163
|
const STOP_HOOK_TIMEOUT_MS = 5_000;
|
|
164
|
+
/** Bun's own `idleTimeout` when `listen()` is given none. */
|
|
165
|
+
const BUN_IDLE_TIMEOUT_SEC = 10;
|
|
141
166
|
/** Same-origin check for a WebSocket handshake (CSWSH default). {@link isSameOriginRequest} is the one
|
|
142
167
|
* owner, shared with the server-function mount in `@nifrajs/web` - the two used to answer differently
|
|
143
168
|
* for the same request, so a browser that could open a socket was told its POST was cross-origin. */
|
|
@@ -173,8 +198,8 @@ function hasUnroutedParam(params) {
|
|
|
173
198
|
return false;
|
|
174
199
|
}
|
|
175
200
|
/**
|
|
176
|
-
* Deno
|
|
177
|
-
* Bun, workerd and
|
|
201
|
+
* Deno's server builds `Request.url` from the Host and target as received, without normalizing the
|
|
202
|
+
* target first. Bun, workerd, and `new Request()` parse it before core sees it.
|
|
178
203
|
*/
|
|
179
204
|
const rawRequestTargets = typeof globalThis.Deno !== "undefined";
|
|
180
205
|
/**
|
|
@@ -200,6 +225,7 @@ function withResolvedTarget(req, url) {
|
|
|
200
225
|
// The same body, so the same framing: a runtime still delimited it.
|
|
201
226
|
if (hasTrustedBodyFraming(req))
|
|
202
227
|
markTrustedBodyFraming(rebuilt);
|
|
228
|
+
carryToCopy(req, rebuilt);
|
|
203
229
|
return rebuilt;
|
|
204
230
|
}
|
|
205
231
|
/**
|
|
@@ -232,7 +258,8 @@ function resolvedSource(req, url) {
|
|
|
232
258
|
// request for `/public` into one for `/admin`. Node refuses such a Host before routing; so does this.
|
|
233
259
|
const HOST_AUTHORITY = /^(?:\[[0-9a-f:.%]+\]|[\w.-]+)(?::(\d{1,5}))?$/i;
|
|
234
260
|
// Clients repeat one Host on every request; comparing to the last valid one is ~7x cheaper than the regex.
|
|
235
|
-
|
|
261
|
+
// `null` until one passes, so an empty Host is never taken as the cached one.
|
|
262
|
+
let lastValidHost = null;
|
|
236
263
|
// A port past 65535, or an IPv6 literal no URL parser takes (`[1:2]`), is refused as Node refuses it:
|
|
237
264
|
// no URL can be built on either, so resolving a dot segment against one threw out of `fetch`.
|
|
238
265
|
const invalidHost = (host) => {
|
|
@@ -253,26 +280,35 @@ const invalidHost = (host) => {
|
|
|
253
280
|
* Host (400), a URL replaces the target, `false` keeps it. Deno hands the target over as sent, so
|
|
254
281
|
* its dot segments are resolved; so does Bun when HTTP/1.0 leaves the Host out, and its route table
|
|
255
282
|
* matched `/files/..` raw. Such a request is served as `localhost`, as Node and Deno serve it.
|
|
256
|
-
* `headerOf`, not `req.headers`: on Deno
|
|
257
|
-
*
|
|
283
|
+
* `headerOf`, not `req.headers`: on Deno it reads Host only when `validateHost` is set, and dot
|
|
284
|
+
* segments are resolved only when `normalizeTarget` is. On Bun only a target that needs resolving
|
|
285
|
+
* is checked unless `validateHost` is set.
|
|
258
286
|
*/
|
|
259
|
-
const requestTarget = (req) => {
|
|
260
|
-
if (
|
|
287
|
+
const requestTarget = (req, validateHost, normalizeTarget) => {
|
|
288
|
+
if (rawRequestTargets) {
|
|
289
|
+
const host = validateHost ? headerOf(req, "host") : null;
|
|
290
|
+
if (host !== null && invalidHost(host))
|
|
291
|
+
return true;
|
|
292
|
+
if (!normalizeTarget || !needsResolving(req.url))
|
|
293
|
+
return false;
|
|
294
|
+
// Deno can preserve an unparseable absolute URL when Host is malformed. Do not route its
|
|
295
|
+
// unresolved dot segments through the raw-target fallback.
|
|
296
|
+
if (!URL.canParse(req.url, "http://localhost"))
|
|
297
|
+
return true;
|
|
298
|
+
return resolveDotSegments(req.url);
|
|
299
|
+
}
|
|
300
|
+
if (!(unparsedBunTargets && (validateHost || needsResolving(req.url))))
|
|
261
301
|
return false;
|
|
262
302
|
const host = headerOf(req, "host");
|
|
263
303
|
if (host !== null && invalidHost(host)) {
|
|
264
|
-
if (rawRequestTargets)
|
|
265
|
-
return true;
|
|
266
304
|
// Bun served it anyway: it could not read the Host, so it left the target unparsed, or it
|
|
267
305
|
// built the URL on it - sent twice (joined with ", ", the URL on the first; RFC 9112 section
|
|
268
|
-
// 3.2 answers 400), percent-encoded (
|
|
269
|
-
// sub-delimiter (
|
|
306
|
+
// 3.2 answers 400), percent-encoded (%61.example routed as a.example), or with a
|
|
307
|
+
// sub-delimiter (a!b). A request built in process keeps the URL its caller built, unless Bun
|
|
270
308
|
// would have built it so.
|
|
271
309
|
const routed = URL.parse(req.url)?.host;
|
|
272
310
|
return routed === undefined || URL.parse(`//${host.split(", ")[0]}`, req.url)?.host === routed;
|
|
273
311
|
}
|
|
274
|
-
if (rawRequestTargets)
|
|
275
|
-
return needsResolving(req.url) && resolveDotSegments(req.url);
|
|
276
312
|
// A request built in process carries an absolute URL whatever its headers say.
|
|
277
313
|
return (host === null &&
|
|
278
314
|
req.url.charCodeAt(0) === 47 /* / */ &&
|
|
@@ -306,7 +342,8 @@ function mountPriorityOf(priority) {
|
|
|
306
342
|
function compareMounts(a, b) {
|
|
307
343
|
return b.priority - a.priority || b.path.length - a.path.length || a.order - b.order;
|
|
308
344
|
}
|
|
309
|
-
/** Rebase a mounted request's path without losing the authentication mark used by child caches
|
|
345
|
+
/** Rebase a mounted request's path without losing the authentication mark used by child caches, or
|
|
346
|
+
* its request locals. */
|
|
310
347
|
function stripMountPrefix(request, prefix) {
|
|
311
348
|
if (prefix === "/")
|
|
312
349
|
return request;
|
|
@@ -316,11 +353,39 @@ function stripMountPrefix(request, prefix) {
|
|
|
316
353
|
const childRequest = new Request(url.href, request);
|
|
317
354
|
if (isAuthenticatedRequest(request))
|
|
318
355
|
markAuthenticatedRequest(childRequest);
|
|
356
|
+
carryToCopy(request, childRequest);
|
|
319
357
|
return childRequest;
|
|
320
358
|
}
|
|
321
359
|
function responseSet(ctx) {
|
|
322
360
|
return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS;
|
|
323
361
|
}
|
|
362
|
+
/** The error hooks of the many fused routes that have none, shared so each does not allocate its own. */
|
|
363
|
+
const NO_ERROR_HOOKS = [];
|
|
364
|
+
const WEB_STAGES = new WeakMap();
|
|
365
|
+
const NODE_STAGES = new WeakMap();
|
|
366
|
+
const hookCount = (hooks) => hooks.length > 1 ? 2 : hooks.length;
|
|
367
|
+
/**
|
|
368
|
+
* The fused stage chain of a hook shape, built once per server and handed to every route with the same
|
|
369
|
+
* number of derive, before and after hooks (none, one or several of each). The chain reads a route's
|
|
370
|
+
* hooks and handler from the `FusedHooks` it is called with, so sharing it changes no answer.
|
|
371
|
+
*/
|
|
372
|
+
function sharedStages(cache, owner, route, build) {
|
|
373
|
+
const derives = hookCount(route.derives);
|
|
374
|
+
const befores = hookCount(route.befores);
|
|
375
|
+
const afters = hookCount(route.afters);
|
|
376
|
+
const key = derives * 9 + befores * 3 + afters;
|
|
377
|
+
let chains = cache.get(owner);
|
|
378
|
+
if (chains === undefined) {
|
|
379
|
+
chains = new Map();
|
|
380
|
+
cache.set(owner, chains);
|
|
381
|
+
}
|
|
382
|
+
let stages = chains.get(key);
|
|
383
|
+
if (stages === undefined) {
|
|
384
|
+
stages = build(derives, befores, afters);
|
|
385
|
+
chains.set(key, stages);
|
|
386
|
+
}
|
|
387
|
+
return stages;
|
|
388
|
+
}
|
|
324
389
|
function isContextlessNoArgArrow(handler) {
|
|
325
390
|
if (handler.length !== 0)
|
|
326
391
|
return false;
|
|
@@ -653,6 +718,8 @@ export class Server {
|
|
|
653
718
|
formOrigins;
|
|
654
719
|
/** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
|
|
655
720
|
trustBodyFraming;
|
|
721
|
+
validateHost;
|
|
722
|
+
normalizeTarget;
|
|
656
723
|
wsMaxPayloadBytes;
|
|
657
724
|
requestTimeoutMs;
|
|
658
725
|
wsUpgradeTimeoutMs;
|
|
@@ -785,6 +852,8 @@ export class Server {
|
|
|
785
852
|
this.protoPoisoning = options.protoPoisoning ?? "reject";
|
|
786
853
|
this.formOrigins = options.formOrigins;
|
|
787
854
|
this.trustBodyFraming = options.trustBodyFraming ?? false;
|
|
855
|
+
this.validateHost = options.validateHost ?? false;
|
|
856
|
+
this.normalizeTarget = options.normalizeTarget ?? false;
|
|
788
857
|
const wsMaxPayloadBytes = options.wsMaxPayloadBytes ?? maxBodyBytes;
|
|
789
858
|
assertByteLimit(wsMaxPayloadBytes, "wsMaxPayloadBytes");
|
|
790
859
|
this.maxBodyBytes = maxBodyBytes;
|
|
@@ -1639,39 +1708,42 @@ export class Server {
|
|
|
1639
1708
|
handler = this.idempotencyRuntime.wrapHandler(handler);
|
|
1640
1709
|
}
|
|
1641
1710
|
const { bare, fusedQuery, fusedBody } = lanes;
|
|
1642
|
-
//
|
|
1643
|
-
//
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1711
|
+
// `selectRouteLanes` admits the fused hook lanes only for derive, before and after hooks, so the
|
|
1712
|
+
// three arrays below are the route's whole lifecycle.
|
|
1713
|
+
const fusedLifecycle = lanes.fusedLane === "hooks" || lanes.fusedLane === "body-hooks";
|
|
1714
|
+
// The fused chains keep the arrays they are built from, and `beforeHandle()` inserts into the live
|
|
1715
|
+
// ones ahead of `beforeHandler()` guards, so the entry and the fused chains share one snapshot of
|
|
1716
|
+
// this route's.
|
|
1717
|
+
const derivesSnapshot = [...this.derives];
|
|
1718
|
+
const beforesSnapshot = [...beforeHandle];
|
|
1719
|
+
const aftersSnapshot = [...afterHandle];
|
|
1720
|
+
const onErrorSnapshot = onError.length === 0 ? NO_ERROR_HOOKS : [...onError];
|
|
1721
|
+
const fusedHooks = fusedLifecycle
|
|
1722
|
+
? {
|
|
1723
|
+
handler: handler,
|
|
1724
|
+
derives: derivesSnapshot,
|
|
1725
|
+
befores: beforesSnapshot,
|
|
1726
|
+
afters: aftersSnapshot,
|
|
1727
|
+
errors: onErrorSnapshot,
|
|
1728
|
+
decorations: hasDecorations ? routeDecorations : undefined,
|
|
1729
|
+
}
|
|
1730
|
+
: undefined;
|
|
1731
|
+
// The Node-direct dispatcher (`!webFast`) prefers these runners over the generic program. Only the
|
|
1732
|
+
// body-only runner also feeds the Web wrapper; the lifecycle lanes build their own Web closure.
|
|
1653
1733
|
const fusedBodyRunner = fusedBody
|
|
1654
1734
|
? this.buildFusedBodyRunner(handler, schema?.body, hasDecorations ? routeDecorations : undefined, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1655
|
-
:
|
|
1656
|
-
? this.
|
|
1657
|
-
:
|
|
1658
|
-
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0], beforeHandle[0], undefined, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1659
|
-
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
1660
|
-
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1661
|
-
: undefined;
|
|
1735
|
+
: fusedHooks !== undefined
|
|
1736
|
+
? this.buildFusedLifecycleRunner(fusedHooks, schema?.query, schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1737
|
+
: undefined;
|
|
1662
1738
|
const fusedWeb = bare && this.aroundHooks.length === 0
|
|
1663
1739
|
? this.buildFusedWeb(handler, hasDecorations ? routeDecorations : undefined, isContextlessNoArgArrow(handler), bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1664
1740
|
: fusedQuery
|
|
1665
1741
|
? this.buildFusedQueryWeb(handler, hasDecorations ? routeDecorations : undefined, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1666
1742
|
: fusedBody
|
|
1667
1743
|
? this.buildFusedBodyWeb(fusedBodyRunner)
|
|
1668
|
-
:
|
|
1669
|
-
? this.
|
|
1670
|
-
:
|
|
1671
|
-
? this.buildFusedDeriveBefore(handler, this.derives[0], beforeHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1672
|
-
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
1673
|
-
? this.buildFusedDeriveBeforeAfter(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1674
|
-
: undefined;
|
|
1744
|
+
: fusedHooks !== undefined
|
|
1745
|
+
? this.buildFusedLifecycleWeb(fusedHooks, schema?.query, schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1746
|
+
: undefined;
|
|
1675
1747
|
const program = compileRouteProgram({
|
|
1676
1748
|
schema,
|
|
1677
1749
|
handler: handler,
|
|
@@ -1703,13 +1775,13 @@ export class Server {
|
|
|
1703
1775
|
idempotent,
|
|
1704
1776
|
ledgered,
|
|
1705
1777
|
responseContract,
|
|
1706
|
-
derives:
|
|
1778
|
+
derives: derivesSnapshot,
|
|
1707
1779
|
authStages: [...compiled.authStages],
|
|
1708
1780
|
decorations: routeDecorations,
|
|
1709
1781
|
hasDecorations,
|
|
1710
|
-
beforeHandle:
|
|
1711
|
-
afterHandle:
|
|
1712
|
-
onError:
|
|
1782
|
+
beforeHandle: beforesSnapshot,
|
|
1783
|
+
afterHandle: aftersSnapshot,
|
|
1784
|
+
onError: onErrorSnapshot,
|
|
1713
1785
|
lifecycleHookLane: lanes.lifecycleHookLane,
|
|
1714
1786
|
around: [...this.aroundHooks],
|
|
1715
1787
|
execution,
|
|
@@ -2110,49 +2182,29 @@ export class Server {
|
|
|
2110
2182
|
const lane = entry.execution.fusedLane;
|
|
2111
2183
|
// Type-erase the lane so each `===` branch sees the full union, not the narrowed remainder.
|
|
2112
2184
|
const laneName = lane;
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2185
|
+
const lifecycle = laneName === "hooks" || laneName === "body-hooks";
|
|
2186
|
+
const hooks = lifecycle
|
|
2187
|
+
? {
|
|
2188
|
+
handler: entry.handler,
|
|
2189
|
+
derives: entry.derives,
|
|
2190
|
+
befores: entry.beforeHandle,
|
|
2191
|
+
afters: entry.afterHandle,
|
|
2192
|
+
errors: entry.onError,
|
|
2193
|
+
decorations: entry.hasDecorations ? entry.decorations : undefined,
|
|
2194
|
+
}
|
|
2195
|
+
: undefined;
|
|
2120
2196
|
const fusedBody = laneName === "body"
|
|
2121
2197
|
? this.buildFusedBodyRunner(entry.handler, entry.schema?.body, entry.hasDecorations ? entry.decorations : undefined, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2122
|
-
:
|
|
2123
|
-
? this.
|
|
2124
|
-
:
|
|
2125
|
-
derive !== undefined &&
|
|
2126
|
-
before !== undefined &&
|
|
2127
|
-
after !== undefined
|
|
2128
|
-
? this.buildFusedBodyDeriveBeforeAfterRunner(entry.handler, derive, before, after, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2129
|
-
: laneName === "derive-before" && derive !== undefined && before !== undefined
|
|
2130
|
-
? this.buildFusedDeriveBeforeRunner(entry.handler, derive, before, undefined, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2131
|
-
: laneName === "derive-before-after" &&
|
|
2132
|
-
derive !== undefined &&
|
|
2133
|
-
before !== undefined &&
|
|
2134
|
-
after !== undefined
|
|
2135
|
-
? this.buildFusedDeriveBeforeRunner(entry.handler, derive, before, after, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2136
|
-
: undefined;
|
|
2198
|
+
: hooks !== undefined
|
|
2199
|
+
? this.buildFusedLifecycleRunner(hooks, entry.schema?.query, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2200
|
+
: undefined;
|
|
2137
2201
|
const fusedWeb = laneName === "body"
|
|
2138
2202
|
? this.buildFusedBodyWeb(fusedBody)
|
|
2139
2203
|
: laneName === "query"
|
|
2140
2204
|
? this.buildFusedQueryWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2141
|
-
:
|
|
2142
|
-
? this.
|
|
2143
|
-
:
|
|
2144
|
-
derive !== undefined &&
|
|
2145
|
-
before !== undefined &&
|
|
2146
|
-
after !== undefined
|
|
2147
|
-
? this.buildFusedBodyDeriveBeforeAfter(entry.handler, derive, before, after, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2148
|
-
: laneName === "derive-before" && derive !== undefined && before !== undefined
|
|
2149
|
-
? this.buildFusedDeriveBefore(entry.handler, derive, before, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2150
|
-
: laneName === "derive-before-after" &&
|
|
2151
|
-
derive !== undefined &&
|
|
2152
|
-
before !== undefined &&
|
|
2153
|
-
after !== undefined
|
|
2154
|
-
? this.buildFusedDeriveBeforeAfter(entry.handler, derive, before, after, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2155
|
-
: this.buildFusedWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, isContextlessNoArgArrow(entry.handler), entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
|
|
2205
|
+
: hooks !== undefined
|
|
2206
|
+
? this.buildFusedLifecycleWeb(hooks, entry.schema?.query, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2207
|
+
: this.buildFusedWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, isContextlessNoArgArrow(entry.handler), entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
|
|
2156
2208
|
return {
|
|
2157
2209
|
...route,
|
|
2158
2210
|
entry: {
|
|
@@ -2185,7 +2237,7 @@ export class Server {
|
|
|
2185
2237
|
fetch(req, platform) {
|
|
2186
2238
|
// Deno's `Request.url` is the target as sent; Bun, workerd and every `new Request()` parse it.
|
|
2187
2239
|
// Route what the parsed URL says, so `/users/../admin` is `/admin` on every runtime.
|
|
2188
|
-
const target = requestTarget(req);
|
|
2240
|
+
const target = requestTarget(req, this.validateHost, this.normalizeTarget);
|
|
2189
2241
|
if (target === true)
|
|
2190
2242
|
return jsonError(400, "invalid_host");
|
|
2191
2243
|
if (target !== false) {
|
|
@@ -2235,6 +2287,23 @@ export class Server {
|
|
|
2235
2287
|
}
|
|
2236
2288
|
return this.admitGated(requestOf(source), () => this.fetchMatchedInner(source, entry, params, platform));
|
|
2237
2289
|
}
|
|
2290
|
+
/** {@link fetchMatched} for an app whose `onRequest` hooks run before the route does: the hooks see
|
|
2291
|
+
* the request first, and the route Bun matched runs unless one of them replaced the request. */
|
|
2292
|
+
fetchMatchedHooked(source, entry, params, platform) {
|
|
2293
|
+
if (this.capacityGate === undefined) {
|
|
2294
|
+
return this.fetchMatchedHookedInner(source, entry, params, platform);
|
|
2295
|
+
}
|
|
2296
|
+
return this.admitGated(source, () => this.fetchMatchedHookedInner(source, entry, params, platform));
|
|
2297
|
+
}
|
|
2298
|
+
fetchMatchedHookedInner(source, entry, params, platform) {
|
|
2299
|
+
const outcome = this.runWithOnRequest(source, this.trustedPlatform(source, platform), this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, true, { entry, params });
|
|
2300
|
+
if (this.onResponseHooks.length === 0 && this.onResponseFinalizedHooks.length === 0) {
|
|
2301
|
+
return outcome;
|
|
2302
|
+
}
|
|
2303
|
+
return outcome instanceof Promise
|
|
2304
|
+
? outcome.then((response) => this.applyOnResponseAndFinalize(response, this.takeResponseRequest(source)))
|
|
2305
|
+
: this.applyOnResponseAndFinalize(outcome, this.takeResponseRequest(source));
|
|
2306
|
+
}
|
|
2238
2307
|
fetchMatchedInner(source, entry, params, platform) {
|
|
2239
2308
|
const outcome = this.runMatched(source, platform, entry, params, undefined, this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, true);
|
|
2240
2309
|
if (this.onResponseHooks.length === 0 && this.onResponseFinalizedHooks.length === 0) {
|
|
@@ -2594,7 +2663,7 @@ export class Server {
|
|
|
2594
2663
|
// The adapter answers a pass by handing this same object to `fetch`.
|
|
2595
2664
|
const received = req;
|
|
2596
2665
|
// The handshake routes the path `fetch` would: Host checked, dot segments resolved (see `fetch`).
|
|
2597
|
-
const target = requestTarget(req);
|
|
2666
|
+
const target = requestTarget(req, this.validateHost, this.normalizeTarget);
|
|
2598
2667
|
if (target === true) {
|
|
2599
2668
|
return { kind: "reject", response: this.wrapWebResponse(jsonError(400, "invalid_host")) };
|
|
2600
2669
|
}
|
|
@@ -2783,10 +2852,10 @@ export class Server {
|
|
|
2783
2852
|
/** Bun `fetch` when WS routes exist: try a WS upgrade first, else run the normal HTTP lifecycle.
|
|
2784
2853
|
* `undefined` ⇒ Bun owns the upgraded socket; a `Response` ⇒ a normal reply or a rejected upgrade.
|
|
2785
2854
|
* (The socket dispatch itself lives in `ws-bun.ts`, loaded via `@nifrajs/core/ws`.) */
|
|
2786
|
-
bunFetchWithWebSocket(req, server) {
|
|
2855
|
+
bunFetchWithWebSocket(req, server, setIdleTimeout) {
|
|
2787
2856
|
const handle = (o) => {
|
|
2788
2857
|
if (o.kind === "pass")
|
|
2789
|
-
return this.fetch(req,
|
|
2858
|
+
return this.fetch(req, new BunPeerPlatform(server, req, setIdleTimeout));
|
|
2790
2859
|
if (o.kind === "reject")
|
|
2791
2860
|
return o.response;
|
|
2792
2861
|
// A copy: the Bun socket keeps per-connection state on it, and a hand-built outcome may be shared.
|
|
@@ -2794,7 +2863,7 @@ export class Server {
|
|
|
2794
2863
|
? undefined
|
|
2795
2864
|
: jsonError(426, "upgrade_required");
|
|
2796
2865
|
};
|
|
2797
|
-
const outcome = this.resolveWebSocketUpgrade(req,
|
|
2866
|
+
const outcome = this.resolveWebSocketUpgrade(req, new BunPeerPlatform(server, req, setIdleTimeout));
|
|
2798
2867
|
return outcome instanceof Promise ? outcome.then(handle) : handle(outcome);
|
|
2799
2868
|
}
|
|
2800
2869
|
/**
|
|
@@ -2895,7 +2964,7 @@ export class Server {
|
|
|
2895
2964
|
* at ~13% of a realistic request. The first hook that returns a Promise hands the REMAINING
|
|
2896
2965
|
* hooks to the async continuation; behavior is identical.
|
|
2897
2966
|
*/
|
|
2898
|
-
runWithOnRequest(source, platform, finalize, wrapResponse, onTimeout, webFast) {
|
|
2967
|
+
runWithOnRequest(source, platform, finalize, wrapResponse, onTimeout, webFast, matched) {
|
|
2899
2968
|
const hooks = this.onRequestHooks;
|
|
2900
2969
|
const originalRequest = requestOf(source);
|
|
2901
2970
|
// A Web Request is already the exact object visible to the hooks and the response walk. Only
|
|
@@ -2914,7 +2983,7 @@ export class Server {
|
|
|
2914
2983
|
return this.onRequestFailure(err, current, platform, finalize, wrapResponse);
|
|
2915
2984
|
}
|
|
2916
2985
|
if (outcome instanceof Promise) {
|
|
2917
|
-
return outcome.then((early) => this.continueOnRequest(early, i + 1, originalRequest, current, platform, finalize, wrapResponse, onTimeout, webFast), (err) => this.onRequestFailure(err, current, platform, finalize, wrapResponse));
|
|
2986
|
+
return outcome.then((early) => this.continueOnRequest(early, i + 1, originalRequest, current, platform, finalize, wrapResponse, onTimeout, webFast, matched), (err) => this.onRequestFailure(err, current, platform, finalize, wrapResponse));
|
|
2918
2987
|
}
|
|
2919
2988
|
if (outcome instanceof Request) {
|
|
2920
2989
|
if (outcome !== seen)
|
|
@@ -2927,11 +2996,11 @@ export class Server {
|
|
|
2927
2996
|
if (outcome !== undefined)
|
|
2928
2997
|
return earlyAnswer(outcome, wrapResponse);
|
|
2929
2998
|
}
|
|
2930
|
-
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast);
|
|
2999
|
+
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast, current === originalRequest ? matched : undefined);
|
|
2931
3000
|
}
|
|
2932
3001
|
/** Async tail of {@link runWithOnRequest}: applies the first awaited hook's outcome, then runs
|
|
2933
3002
|
* the remaining hooks (awaiting freely - we're already async here). */
|
|
2934
|
-
async continueOnRequest(first, nextIndex, originalRequest, sourceAtAwait, platform, finalize, wrapResponse, onTimeout, webFast) {
|
|
3003
|
+
async continueOnRequest(first, nextIndex, originalRequest, sourceAtAwait, platform, finalize, wrapResponse, onTimeout, webFast, matched) {
|
|
2935
3004
|
let current = sourceAtAwait;
|
|
2936
3005
|
let seen = requestOf(sourceAtAwait);
|
|
2937
3006
|
let early = first;
|
|
@@ -2959,7 +3028,7 @@ export class Server {
|
|
|
2959
3028
|
}
|
|
2960
3029
|
index++;
|
|
2961
3030
|
}
|
|
2962
|
-
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast);
|
|
3031
|
+
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast, current === originalRequest ? matched : undefined);
|
|
2963
3032
|
}
|
|
2964
3033
|
takeResponseRequest(source) {
|
|
2965
3034
|
const tracked = this.responseSources.get(source);
|
|
@@ -3028,7 +3097,12 @@ export class Server {
|
|
|
3028
3097
|
* (selected by its compiled plan), so a sync handler produces its result with zero promise allocations;
|
|
3029
3098
|
* routes with validation/hooks keep the full compiled route program, unchanged.
|
|
3030
3099
|
*/
|
|
3031
|
-
routeAndRun(source, platform, finalize, wrapResponse, onTimeout, webFast
|
|
3100
|
+
routeAndRun(source, platform, finalize, wrapResponse, onTimeout, webFast,
|
|
3101
|
+
// The route Bun's table already matched for this very request, which routing would find again.
|
|
3102
|
+
matched) {
|
|
3103
|
+
if (matched !== undefined) {
|
|
3104
|
+
return this.runMatched(source, platform, matched.entry, matched.params, undefined, finalize, wrapResponse, onTimeout, webFast);
|
|
3105
|
+
}
|
|
3032
3106
|
// Native delimiter searches keep path/query splitting out of a character-by-character JS loop.
|
|
3033
3107
|
// Keep the slices in locals instead of allocating the public helper's pathname/search pair.
|
|
3034
3108
|
let pathname;
|
|
@@ -3518,53 +3592,244 @@ export class Server {
|
|
|
3518
3592
|
};
|
|
3519
3593
|
}
|
|
3520
3594
|
/**
|
|
3521
|
-
*
|
|
3522
|
-
*
|
|
3523
|
-
*
|
|
3524
|
-
*
|
|
3525
|
-
* and
|
|
3526
|
-
* hot hidden class. Behavior is byte-identical to the compiled route program: the same thrown-value
|
|
3527
|
-
* contract (`Response` is control flow, anything else logs + 500), the same `c.set` semantics, the
|
|
3528
|
-
* same `fusedRespond` for the rendered body. The `LifecycleExecutionLane` guarantee
|
|
3529
|
-
* (`schema.params === undefined && schema.headers === undefined && derives === 1 &&
|
|
3530
|
-
* beforeHandle === 1 && afterHandle === 0 && onError === 0 && !hasResponseContract &&
|
|
3531
|
-
* !hasDecorations`) is enforced by `selectRouteLanes`; this lane is selected ONLY when all of those
|
|
3532
|
-
* hold. A `query` schema (the route declares `{ query }` alongside the hooks) is validated FIRST,
|
|
3533
|
-
* mirroring the compiled route program's stage order; the fused builder runs validate → derive →
|
|
3534
|
-
* before → handler in one frame.
|
|
3595
|
+
* The stage chain a hook shape runs - derive, before, handler, after, respond - for the Web lane,
|
|
3596
|
+
* built once per server and shared by every route with the same number of hooks in each phase (none,
|
|
3597
|
+
* one or several). The chain reads a route's handler and hooks from its `FusedHooks` per request, so
|
|
3598
|
+
* a route's own closure (see {@link buildFusedLifecycleWeb}) holds only that record and its schemas,
|
|
3599
|
+
* and memory grows with the number of distinct shapes, not routes or hook functions.
|
|
3535
3600
|
*/
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3601
|
+
fusedWebStages(deriveCount, beforeCount, afterCount) {
|
|
3602
|
+
let runAfter;
|
|
3603
|
+
if (afterCount === 0) {
|
|
3604
|
+
runAfter = (result, ctx, route) => this.respondHooks(result, ctx, route, this.onResponseHooks.length === 0);
|
|
3605
|
+
}
|
|
3606
|
+
else if (afterCount === 1) {
|
|
3607
|
+
runAfter = (result, ctx, route) => {
|
|
3608
|
+
let transformed;
|
|
3609
|
+
try {
|
|
3610
|
+
transformed = route.afters[0](result, ctx);
|
|
3611
|
+
}
|
|
3612
|
+
catch (err) {
|
|
3613
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3614
|
+
}
|
|
3615
|
+
if (transformed instanceof Promise) {
|
|
3616
|
+
return transformed.then((value) => this.respondHooks(value, ctx, route, this.onResponseHooks.length === 0), (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3617
|
+
}
|
|
3618
|
+
return this.respondHooks(transformed, ctx, route, this.onResponseHooks.length === 0);
|
|
3619
|
+
};
|
|
3620
|
+
}
|
|
3621
|
+
else {
|
|
3622
|
+
runAfter = (result, ctx, route, index) => {
|
|
3623
|
+
const lastAfter = route.afters.length - 1;
|
|
3624
|
+
for (;;) {
|
|
3625
|
+
let transformed;
|
|
3626
|
+
try {
|
|
3627
|
+
transformed = route.afters[index](result, ctx);
|
|
3628
|
+
}
|
|
3629
|
+
catch (err) {
|
|
3630
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3631
|
+
}
|
|
3632
|
+
if (transformed instanceof Promise) {
|
|
3633
|
+
return transformed.then((value) => index < lastAfter
|
|
3634
|
+
? runAfter(value, ctx, route, index + 1)
|
|
3635
|
+
: this.respondHooks(value, ctx, route, this.onResponseHooks.length === 0), (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3636
|
+
}
|
|
3637
|
+
if (index === lastAfter) {
|
|
3638
|
+
return this.respondHooks(transformed, ctx, route, this.onResponseHooks.length === 0);
|
|
3639
|
+
}
|
|
3640
|
+
result = transformed;
|
|
3641
|
+
index++;
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
3645
|
+
const runHandler = (ctx, route) => {
|
|
3646
|
+
let result;
|
|
3547
3647
|
try {
|
|
3548
|
-
|
|
3648
|
+
result = route.handler(ctx);
|
|
3549
3649
|
}
|
|
3550
3650
|
catch (err) {
|
|
3551
|
-
return
|
|
3651
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3552
3652
|
}
|
|
3553
|
-
if (
|
|
3554
|
-
return
|
|
3653
|
+
if (result instanceof Promise) {
|
|
3654
|
+
return result.then((value) => runAfter(value, ctx, route, 0), (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3555
3655
|
}
|
|
3556
|
-
return
|
|
3656
|
+
return runAfter(result, ctx, route, 0);
|
|
3557
3657
|
};
|
|
3658
|
+
let runBefore;
|
|
3659
|
+
if (beforeCount === 0) {
|
|
3660
|
+
runBefore = runHandler;
|
|
3661
|
+
}
|
|
3662
|
+
else if (beforeCount === 1) {
|
|
3663
|
+
runBefore = (ctx, route) => {
|
|
3664
|
+
let early;
|
|
3665
|
+
try {
|
|
3666
|
+
early = route.befores[0](ctx);
|
|
3667
|
+
}
|
|
3668
|
+
catch (err) {
|
|
3669
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3670
|
+
}
|
|
3671
|
+
if (early instanceof Promise) {
|
|
3672
|
+
return early.then((settled) => settled === undefined
|
|
3673
|
+
? runHandler(ctx, route, 0)
|
|
3674
|
+
: this.respondHooks(settled, ctx, route), (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3675
|
+
}
|
|
3676
|
+
if (early === undefined)
|
|
3677
|
+
return runHandler(ctx, route, 0);
|
|
3678
|
+
return this.respondHooks(early, ctx, route);
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
else {
|
|
3682
|
+
runBefore = (ctx, route, index) => {
|
|
3683
|
+
const lastBefore = route.befores.length - 1;
|
|
3684
|
+
for (;;) {
|
|
3685
|
+
let early;
|
|
3686
|
+
try {
|
|
3687
|
+
early = route.befores[index](ctx);
|
|
3688
|
+
}
|
|
3689
|
+
catch (err) {
|
|
3690
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3691
|
+
}
|
|
3692
|
+
if (early instanceof Promise) {
|
|
3693
|
+
return early.then((settled) => {
|
|
3694
|
+
if (settled !== undefined)
|
|
3695
|
+
return this.respondHooks(settled, ctx, route);
|
|
3696
|
+
return index < lastBefore
|
|
3697
|
+
? runBefore(ctx, route, index + 1)
|
|
3698
|
+
: runHandler(ctx, route, 0);
|
|
3699
|
+
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3700
|
+
}
|
|
3701
|
+
if (early !== undefined)
|
|
3702
|
+
return this.respondHooks(early, ctx, route);
|
|
3703
|
+
if (index === lastBefore)
|
|
3704
|
+
return runHandler(ctx, route, 0);
|
|
3705
|
+
index++;
|
|
3706
|
+
}
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3709
|
+
let runDerive;
|
|
3710
|
+
if (deriveCount === 0) {
|
|
3711
|
+
runDerive = runBefore;
|
|
3712
|
+
}
|
|
3713
|
+
else if (deriveCount === 1) {
|
|
3714
|
+
runDerive = (ctx, route) => {
|
|
3715
|
+
let derived;
|
|
3716
|
+
try {
|
|
3717
|
+
derived = route.derives[0](ctx);
|
|
3718
|
+
}
|
|
3719
|
+
catch (err) {
|
|
3720
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3721
|
+
}
|
|
3722
|
+
if (derived instanceof Promise) {
|
|
3723
|
+
return derived.then((settled) => {
|
|
3724
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3725
|
+
return this.respondHooks(settled, ctx, route);
|
|
3726
|
+
}
|
|
3727
|
+
try {
|
|
3728
|
+
Object.assign(ctx, settled);
|
|
3729
|
+
}
|
|
3730
|
+
catch (err) {
|
|
3731
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3732
|
+
}
|
|
3733
|
+
return runBefore(ctx, route, 0);
|
|
3734
|
+
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3735
|
+
}
|
|
3736
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3737
|
+
return this.respondHooks(derived, ctx, route);
|
|
3738
|
+
}
|
|
3739
|
+
try {
|
|
3740
|
+
Object.assign(ctx, derived);
|
|
3741
|
+
}
|
|
3742
|
+
catch (err) {
|
|
3743
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3744
|
+
}
|
|
3745
|
+
return runBefore(ctx, route, 0);
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
else {
|
|
3749
|
+
runDerive = (ctx, route, index) => {
|
|
3750
|
+
const lastDerive = route.derives.length - 1;
|
|
3751
|
+
for (;;) {
|
|
3752
|
+
let derived;
|
|
3753
|
+
try {
|
|
3754
|
+
derived = route.derives[index](ctx);
|
|
3755
|
+
}
|
|
3756
|
+
catch (err) {
|
|
3757
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3758
|
+
}
|
|
3759
|
+
if (derived instanceof Promise) {
|
|
3760
|
+
return derived.then((settled) => {
|
|
3761
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3762
|
+
return this.respondHooks(settled, ctx, route);
|
|
3763
|
+
}
|
|
3764
|
+
try {
|
|
3765
|
+
Object.assign(ctx, settled);
|
|
3766
|
+
}
|
|
3767
|
+
catch (err) {
|
|
3768
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3769
|
+
}
|
|
3770
|
+
return index < lastDerive
|
|
3771
|
+
? runDerive(ctx, route, index + 1)
|
|
3772
|
+
: runBefore(ctx, route, 0);
|
|
3773
|
+
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3774
|
+
}
|
|
3775
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3776
|
+
return this.respondHooks(derived, ctx, route);
|
|
3777
|
+
}
|
|
3778
|
+
try {
|
|
3779
|
+
Object.assign(ctx, derived);
|
|
3780
|
+
}
|
|
3781
|
+
catch (err) {
|
|
3782
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3783
|
+
}
|
|
3784
|
+
if (index === lastDerive)
|
|
3785
|
+
return runBefore(ctx, route, 0);
|
|
3786
|
+
index++;
|
|
3787
|
+
}
|
|
3788
|
+
};
|
|
3789
|
+
}
|
|
3790
|
+
return runDerive;
|
|
3791
|
+
}
|
|
3792
|
+
/**
|
|
3793
|
+
* Compile a fused lifecycle route's entry closure: parse (body) → validate → the shared stage chain
|
|
3794
|
+
* of its hook shape (derive → before → handler → after → respond, see {@link fusedWebStages}) in a
|
|
3795
|
+
* single frame, where the generic program walks staged hook loops. `selectRouteLanes` admits only
|
|
3796
|
+
* derive, before and after hooks - no params or headers schema, onError, around, decorations,
|
|
3797
|
+
* response contract or recovery hook, and never a query schema beside a body schema - so the behavior
|
|
3798
|
+
* is the compiled program's: the same stage order, the same thrown-value contract (`Response` is
|
|
3799
|
+
* control flow, anything else logs and 500s), the same `c.set` semantics and `fusedRespond` render.
|
|
3800
|
+
* The route's own closure holds only its `FusedHooks` and schemas and takes the request's context as
|
|
3801
|
+
* an argument, so a request allocates no step closures.
|
|
3802
|
+
*/
|
|
3803
|
+
/**
|
|
3804
|
+
* Keep the common one-derive/one-before shape bound to its hooks. Inline its synchronous path to
|
|
3805
|
+
* avoid stage calls; retain helper closures only for promise continuations.
|
|
3806
|
+
*/
|
|
3807
|
+
buildFusedLifecycleWebSingle(handler, derive, before, after, maxBodyBytes) {
|
|
3808
|
+
const runAfter = after === undefined
|
|
3809
|
+
? (result, ctx) => this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
3810
|
+
: (result, ctx) => {
|
|
3811
|
+
let transformed;
|
|
3812
|
+
try {
|
|
3813
|
+
transformed = after(result, ctx);
|
|
3814
|
+
}
|
|
3815
|
+
catch (err) {
|
|
3816
|
+
return this.fusedWebFail(err, ctx);
|
|
3817
|
+
}
|
|
3818
|
+
if (transformed instanceof Promise) {
|
|
3819
|
+
return transformed.then((value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0), (err) => this.fusedWebFail(err, ctx));
|
|
3820
|
+
}
|
|
3821
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0);
|
|
3822
|
+
};
|
|
3558
3823
|
const runHandler = (ctx) => {
|
|
3559
3824
|
let result;
|
|
3560
3825
|
try {
|
|
3561
3826
|
result = handler(ctx);
|
|
3562
3827
|
}
|
|
3563
3828
|
catch (err) {
|
|
3564
|
-
return
|
|
3829
|
+
return this.fusedWebFail(err, ctx);
|
|
3565
3830
|
}
|
|
3566
3831
|
if (result instanceof Promise) {
|
|
3567
|
-
return result.then((value) => runAfter(value, ctx), (err) =>
|
|
3832
|
+
return result.then((value) => runAfter(value, ctx), (err) => this.fusedWebFail(err, ctx));
|
|
3568
3833
|
}
|
|
3569
3834
|
return runAfter(result, ctx);
|
|
3570
3835
|
};
|
|
@@ -3574,461 +3839,463 @@ export class Server {
|
|
|
3574
3839
|
early = before(ctx);
|
|
3575
3840
|
}
|
|
3576
3841
|
catch (err) {
|
|
3577
|
-
return
|
|
3842
|
+
return this.fusedWebFail(err, ctx);
|
|
3578
3843
|
}
|
|
3579
3844
|
if (early instanceof Promise) {
|
|
3580
|
-
return early.then((settled) =>
|
|
3581
|
-
if (settled === undefined)
|
|
3582
|
-
return runHandler(ctx);
|
|
3583
|
-
return this.respondFused(settled, ctx);
|
|
3584
|
-
}, (err) => logError(err, ctx));
|
|
3845
|
+
return early.then((settled) => (settled === undefined ? runHandler(ctx) : this.respondFused(settled, ctx)), (err) => this.fusedWebFail(err, ctx));
|
|
3585
3846
|
}
|
|
3586
3847
|
if (early === undefined)
|
|
3587
3848
|
return runHandler(ctx);
|
|
3588
3849
|
return this.respondFused(early, ctx);
|
|
3589
3850
|
};
|
|
3590
|
-
|
|
3851
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3852
|
+
const ctx = nativeContext
|
|
3853
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3854
|
+
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
3591
3855
|
let derived;
|
|
3592
3856
|
try {
|
|
3593
3857
|
derived = derive(ctx);
|
|
3594
3858
|
}
|
|
3595
3859
|
catch (err) {
|
|
3596
|
-
return
|
|
3860
|
+
return this.fusedWebFail(err, ctx);
|
|
3597
3861
|
}
|
|
3598
3862
|
if (derived instanceof Promise) {
|
|
3599
3863
|
return derived.then((settled) => {
|
|
3600
|
-
if (isResponseResult(settled) || settled instanceof Response)
|
|
3864
|
+
if (isResponseResult(settled) || settled instanceof Response)
|
|
3601
3865
|
return this.respondFused(settled, ctx);
|
|
3866
|
+
try {
|
|
3867
|
+
Object.assign(ctx, settled);
|
|
3868
|
+
}
|
|
3869
|
+
catch (err) {
|
|
3870
|
+
return this.fusedWebFail(err, ctx);
|
|
3602
3871
|
}
|
|
3603
|
-
Object.assign(ctx, settled);
|
|
3604
3872
|
return runBefore(ctx);
|
|
3605
|
-
}, (err) =>
|
|
3873
|
+
}, (err) => this.fusedWebFail(err, ctx));
|
|
3606
3874
|
}
|
|
3607
|
-
if (isResponseResult(derived) || derived instanceof Response)
|
|
3875
|
+
if (isResponseResult(derived) || derived instanceof Response)
|
|
3608
3876
|
return this.respondFused(derived, ctx);
|
|
3877
|
+
let early;
|
|
3878
|
+
let result;
|
|
3879
|
+
let transformed;
|
|
3880
|
+
let handlerPromise;
|
|
3881
|
+
try {
|
|
3882
|
+
Object.assign(ctx, derived);
|
|
3883
|
+
early = before(ctx);
|
|
3884
|
+
if (early === undefined) {
|
|
3885
|
+
result = handler(ctx);
|
|
3886
|
+
if (result instanceof Promise)
|
|
3887
|
+
handlerPromise = result;
|
|
3888
|
+
else if (after !== undefined)
|
|
3889
|
+
transformed = after(result, ctx);
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
catch (err) {
|
|
3893
|
+
return this.fusedWebFail(err, ctx);
|
|
3894
|
+
}
|
|
3895
|
+
if (early instanceof Promise) {
|
|
3896
|
+
return early.then((settled) => (settled === undefined ? runHandler(ctx) : this.respondFused(settled, ctx)), (err) => this.fusedWebFail(err, ctx));
|
|
3609
3897
|
}
|
|
3610
|
-
|
|
3611
|
-
|
|
3898
|
+
if (early !== undefined)
|
|
3899
|
+
return this.respondFused(early, ctx);
|
|
3900
|
+
if (handlerPromise !== undefined) {
|
|
3901
|
+
return handlerPromise.then((value) => after === undefined
|
|
3902
|
+
? this.respondFused(value, ctx, this.onResponseHooks.length === 0)
|
|
3903
|
+
: runAfter(value, ctx), (err) => this.fusedWebFail(err, ctx));
|
|
3904
|
+
}
|
|
3905
|
+
if (after === undefined)
|
|
3906
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0);
|
|
3907
|
+
if (transformed instanceof Promise) {
|
|
3908
|
+
return transformed.then((value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0), (err) => this.fusedWebFail(err, ctx));
|
|
3909
|
+
}
|
|
3910
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0);
|
|
3612
3911
|
};
|
|
3912
|
+
}
|
|
3913
|
+
buildFusedLifecycleWeb(route, querySchema, bodySchema, maxBodyBytes) {
|
|
3914
|
+
if (querySchema === undefined &&
|
|
3915
|
+
bodySchema === undefined &&
|
|
3916
|
+
route.errors.length === 0 &&
|
|
3917
|
+
route.decorations === undefined &&
|
|
3918
|
+
route.derives.length === 1 &&
|
|
3919
|
+
route.befores.length === 1 &&
|
|
3920
|
+
route.afters.length <= 1) {
|
|
3921
|
+
const derive = route.derives[0];
|
|
3922
|
+
const before = route.befores[0];
|
|
3923
|
+
if (derive !== undefined && before !== undefined) {
|
|
3924
|
+
return this.buildFusedLifecycleWebSingle(route.handler, derive, before, route.afters[0], maxBodyBytes);
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
const runDerive = sharedStages(WEB_STAGES, this, route, (d, b, a) => this.fusedWebStages(d, b, a));
|
|
3928
|
+
const decorations = route.decorations;
|
|
3929
|
+
if (bodySchema !== undefined) {
|
|
3930
|
+
const onParsed = (parsed, ctx) => {
|
|
3931
|
+
let validation;
|
|
3932
|
+
try {
|
|
3933
|
+
validation = bodySchema["~standard"].validate(parsed);
|
|
3934
|
+
}
|
|
3935
|
+
catch (err) {
|
|
3936
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3937
|
+
}
|
|
3938
|
+
if (validation instanceof Promise) {
|
|
3939
|
+
return validation.then((settled) => {
|
|
3940
|
+
if (settled.issues !== undefined)
|
|
3941
|
+
return this.wrapWebResponse(plainValidationError(settled.issues));
|
|
3942
|
+
ctx.body = settled.value;
|
|
3943
|
+
return runDerive(ctx, route, 0);
|
|
3944
|
+
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3945
|
+
}
|
|
3946
|
+
if (validation.issues !== undefined)
|
|
3947
|
+
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
3948
|
+
ctx.body = validation.value;
|
|
3949
|
+
return runDerive(ctx, route, 0);
|
|
3950
|
+
};
|
|
3951
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3952
|
+
const ctx = nativeContext
|
|
3953
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3954
|
+
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
3955
|
+
if (decorations !== undefined)
|
|
3956
|
+
Object.assign(ctx, decorations);
|
|
3957
|
+
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, ctx), this.wrapWebResponse, (err) => this.fusedWebRouteFail(err, ctx, route), bodySchema, this.formOrigins);
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3613
3960
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3614
3961
|
const ctx = nativeContext
|
|
3615
3962
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3616
3963
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
3964
|
+
if (decorations !== undefined)
|
|
3965
|
+
Object.assign(ctx, decorations);
|
|
3617
3966
|
if (querySchema === undefined)
|
|
3618
|
-
return runDerive(ctx);
|
|
3967
|
+
return runDerive(ctx, route, 0);
|
|
3619
3968
|
let validation;
|
|
3620
3969
|
try {
|
|
3621
3970
|
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
|
|
3622
3971
|
}
|
|
3623
3972
|
catch (err) {
|
|
3624
|
-
return
|
|
3973
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
3625
3974
|
}
|
|
3626
3975
|
if (validation instanceof Promise) {
|
|
3627
3976
|
return validation.then((settled) => {
|
|
3628
3977
|
if (settled.issues !== undefined)
|
|
3629
3978
|
return this.wrapWebResponse(plainValidationError(settled.issues));
|
|
3630
3979
|
ctx.query = settled.value;
|
|
3631
|
-
return runDerive(ctx);
|
|
3632
|
-
}, (err) =>
|
|
3980
|
+
return runDerive(ctx, route, 0);
|
|
3981
|
+
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3633
3982
|
}
|
|
3634
3983
|
if (validation.issues !== undefined)
|
|
3635
3984
|
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
3636
3985
|
ctx.query = validation.value;
|
|
3637
|
-
return runDerive(ctx);
|
|
3986
|
+
return runDerive(ctx, route, 0);
|
|
3638
3987
|
};
|
|
3639
3988
|
}
|
|
3640
3989
|
/**
|
|
3641
|
-
*
|
|
3642
|
-
*
|
|
3643
|
-
*
|
|
3644
|
-
* ONLY when `schema.body !== undefined && schema.query/params/headers === undefined &&
|
|
3645
|
-
* schema.onValidationError === undefined && !defaultOnValidationError && !hasIdempotency &&
|
|
3646
|
-
* !hasLedger && derives === 1 && beforeHandle === 1 && afterHandle <= 1 && onError === 0 &&
|
|
3647
|
-
* !hasResponseContract && !hasDecorations && around === 0`. The same single `querySchema === undefined`
|
|
3648
|
-
* trick used in the no-body lane lets the build path stay simple: a query + body route falls back
|
|
3649
|
-
* to the generic program (this is rare in practice - validated query + validated body is usually a
|
|
3650
|
-
* "search results" route with a middleware chain on top, and the generic lane is fine there).
|
|
3990
|
+
* The Node-direct twin of {@link fusedWebStages}: the same chain, finalizing through the caller's
|
|
3991
|
+
* own `finalize`, shared by every route with the same hook shape. A terminal `finalize` throw (an
|
|
3992
|
+
* unstringifiable handler return on the Node lane) is caught as `finishLifecycleContract` catches it.
|
|
3651
3993
|
*/
|
|
3652
|
-
|
|
3653
|
-
const
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3994
|
+
fusedNodeStages(deriveCount, beforeCount, afterCount) {
|
|
3995
|
+
const finishResult = (result, ctx, route, finalize, wrapResponse) => {
|
|
3996
|
+
try {
|
|
3997
|
+
return finalize(result, responseSet(ctx), ctx);
|
|
3998
|
+
}
|
|
3999
|
+
catch (err) {
|
|
4000
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
let runAfter;
|
|
4004
|
+
if (afterCount === 0) {
|
|
4005
|
+
runAfter = (result, ctx, route, finalize, wrapResponse) => finishResult(result, ctx, route, finalize, wrapResponse);
|
|
4006
|
+
}
|
|
4007
|
+
else if (afterCount === 1) {
|
|
4008
|
+
runAfter = (result, ctx, route, finalize, wrapResponse) => {
|
|
4009
|
+
let transformed;
|
|
4010
|
+
try {
|
|
4011
|
+
transformed = route.afters[0](result, ctx);
|
|
4012
|
+
}
|
|
4013
|
+
catch (err) {
|
|
4014
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4015
|
+
}
|
|
4016
|
+
if (transformed instanceof Promise) {
|
|
4017
|
+
return transformed.then((value) => finishResult(value, ctx, route, finalize, wrapResponse), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
4018
|
+
}
|
|
4019
|
+
return finishResult(transformed, ctx, route, finalize, wrapResponse);
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
else {
|
|
4023
|
+
runAfter = (result, ctx, route, finalize, wrapResponse, index) => {
|
|
4024
|
+
const lastAfter = route.afters.length - 1;
|
|
4025
|
+
for (;;) {
|
|
3663
4026
|
let transformed;
|
|
3664
4027
|
try {
|
|
3665
|
-
transformed =
|
|
4028
|
+
transformed = route.afters[index](result, ctx);
|
|
3666
4029
|
}
|
|
3667
4030
|
catch (err) {
|
|
3668
|
-
return
|
|
4031
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
3669
4032
|
}
|
|
3670
4033
|
if (transformed instanceof Promise) {
|
|
3671
|
-
return transformed.then((value) =>
|
|
4034
|
+
return transformed.then((value) => index < lastAfter
|
|
4035
|
+
? runAfter(value, ctx, route, finalize, wrapResponse, index + 1)
|
|
4036
|
+
: finishResult(value, ctx, route, finalize, wrapResponse), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
3672
4037
|
}
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
4038
|
+
if (index === lastAfter)
|
|
4039
|
+
return finishResult(transformed, ctx, route, finalize, wrapResponse);
|
|
4040
|
+
result = transformed;
|
|
4041
|
+
index++;
|
|
4042
|
+
}
|
|
4043
|
+
};
|
|
4044
|
+
}
|
|
4045
|
+
const runHandler = (ctx, route, finalize, wrapResponse) => {
|
|
4046
|
+
let result;
|
|
4047
|
+
try {
|
|
4048
|
+
result = route.handler(ctx);
|
|
4049
|
+
}
|
|
4050
|
+
catch (err) {
|
|
4051
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4052
|
+
}
|
|
4053
|
+
if (result instanceof Promise) {
|
|
4054
|
+
return result.then((value) => runAfter(value, ctx, route, finalize, wrapResponse, 0), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
4055
|
+
}
|
|
4056
|
+
return runAfter(result, ctx, route, finalize, wrapResponse, 0);
|
|
4057
|
+
};
|
|
4058
|
+
let runBefore;
|
|
4059
|
+
if (beforeCount === 0) {
|
|
4060
|
+
runBefore = runHandler;
|
|
4061
|
+
}
|
|
4062
|
+
else if (beforeCount === 1) {
|
|
4063
|
+
runBefore = (ctx, route, finalize, wrapResponse) => {
|
|
4064
|
+
let early;
|
|
4065
|
+
try {
|
|
4066
|
+
early = route.befores[0](ctx);
|
|
4067
|
+
}
|
|
4068
|
+
catch (err) {
|
|
4069
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4070
|
+
}
|
|
4071
|
+
if (early instanceof Promise) {
|
|
4072
|
+
return early.then((settled) => settled === undefined
|
|
4073
|
+
? runHandler(ctx, route, finalize, wrapResponse, 0)
|
|
4074
|
+
: finishResult(settled, ctx, route, finalize, wrapResponse), (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
4075
|
+
}
|
|
4076
|
+
if (early === undefined)
|
|
4077
|
+
return runHandler(ctx, route, finalize, wrapResponse, 0);
|
|
4078
|
+
return finishResult(early, ctx, route, finalize, wrapResponse);
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
else {
|
|
4082
|
+
runBefore = (ctx, route, finalize, wrapResponse, index) => {
|
|
4083
|
+
const lastBefore = route.befores.length - 1;
|
|
4084
|
+
for (;;) {
|
|
3689
4085
|
let early;
|
|
3690
4086
|
try {
|
|
3691
|
-
early =
|
|
4087
|
+
early = route.befores[index](ctx);
|
|
3692
4088
|
}
|
|
3693
4089
|
catch (err) {
|
|
3694
|
-
return
|
|
4090
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
3695
4091
|
}
|
|
3696
4092
|
if (early instanceof Promise) {
|
|
3697
4093
|
return early.then((settled) => {
|
|
3698
|
-
if (settled
|
|
3699
|
-
return
|
|
3700
|
-
return
|
|
3701
|
-
|
|
4094
|
+
if (settled !== undefined)
|
|
4095
|
+
return finishResult(settled, ctx, route, finalize, wrapResponse);
|
|
4096
|
+
return index < lastBefore
|
|
4097
|
+
? runBefore(ctx, route, finalize, wrapResponse, index + 1)
|
|
4098
|
+
: runHandler(ctx, route, finalize, wrapResponse, 0);
|
|
4099
|
+
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
3702
4100
|
}
|
|
3703
|
-
if (early
|
|
3704
|
-
return
|
|
3705
|
-
|
|
3706
|
-
|
|
4101
|
+
if (early !== undefined)
|
|
4102
|
+
return finishResult(early, ctx, route, finalize, wrapResponse);
|
|
4103
|
+
if (index === lastBefore)
|
|
4104
|
+
return runHandler(ctx, route, finalize, wrapResponse, 0);
|
|
4105
|
+
index++;
|
|
4106
|
+
}
|
|
4107
|
+
};
|
|
4108
|
+
}
|
|
4109
|
+
let runDerive;
|
|
4110
|
+
if (deriveCount === 0) {
|
|
4111
|
+
runDerive = runBefore;
|
|
4112
|
+
}
|
|
4113
|
+
else if (deriveCount === 1) {
|
|
4114
|
+
runDerive = (ctx, route, finalize, wrapResponse) => {
|
|
3707
4115
|
let derived;
|
|
3708
4116
|
try {
|
|
3709
|
-
derived =
|
|
4117
|
+
derived = route.derives[0](ctx);
|
|
3710
4118
|
}
|
|
3711
4119
|
catch (err) {
|
|
3712
|
-
return
|
|
4120
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
3713
4121
|
}
|
|
3714
4122
|
if (derived instanceof Promise) {
|
|
3715
4123
|
return derived.then((settled) => {
|
|
3716
4124
|
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3717
|
-
return
|
|
4125
|
+
return finishResult(settled, ctx, route, finalize, wrapResponse);
|
|
3718
4126
|
}
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
4127
|
+
try {
|
|
4128
|
+
Object.assign(ctx, settled);
|
|
4129
|
+
}
|
|
4130
|
+
catch (err) {
|
|
4131
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4132
|
+
}
|
|
4133
|
+
return runBefore(ctx, route, finalize, wrapResponse, 0);
|
|
4134
|
+
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
3722
4135
|
}
|
|
3723
4136
|
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3724
|
-
return
|
|
4137
|
+
return finishResult(derived, ctx, route, finalize, wrapResponse);
|
|
3725
4138
|
}
|
|
3726
|
-
|
|
3727
|
-
|
|
4139
|
+
try {
|
|
4140
|
+
Object.assign(ctx, derived);
|
|
4141
|
+
}
|
|
4142
|
+
catch (err) {
|
|
4143
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4144
|
+
}
|
|
4145
|
+
return runBefore(ctx, route, finalize, wrapResponse, 0);
|
|
3728
4146
|
};
|
|
3729
|
-
|
|
4147
|
+
}
|
|
4148
|
+
else {
|
|
4149
|
+
runDerive = (ctx, route, finalize, wrapResponse, index) => {
|
|
4150
|
+
const lastDerive = route.derives.length - 1;
|
|
4151
|
+
for (;;) {
|
|
4152
|
+
let derived;
|
|
4153
|
+
try {
|
|
4154
|
+
derived = route.derives[index](ctx);
|
|
4155
|
+
}
|
|
4156
|
+
catch (err) {
|
|
4157
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4158
|
+
}
|
|
4159
|
+
if (derived instanceof Promise) {
|
|
4160
|
+
return derived.then((settled) => {
|
|
4161
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
4162
|
+
return finishResult(settled, ctx, route, finalize, wrapResponse);
|
|
4163
|
+
}
|
|
4164
|
+
try {
|
|
4165
|
+
Object.assign(ctx, settled);
|
|
4166
|
+
}
|
|
4167
|
+
catch (err) {
|
|
4168
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4169
|
+
}
|
|
4170
|
+
return index < lastDerive
|
|
4171
|
+
? runDerive(ctx, route, finalize, wrapResponse, index + 1)
|
|
4172
|
+
: runBefore(ctx, route, finalize, wrapResponse, 0);
|
|
4173
|
+
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
4174
|
+
}
|
|
4175
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
4176
|
+
return finishResult(derived, ctx, route, finalize, wrapResponse);
|
|
4177
|
+
}
|
|
4178
|
+
try {
|
|
4179
|
+
Object.assign(ctx, derived);
|
|
4180
|
+
}
|
|
4181
|
+
catch (err) {
|
|
4182
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4183
|
+
}
|
|
4184
|
+
if (index === lastDerive)
|
|
4185
|
+
return runBefore(ctx, route, finalize, wrapResponse, 0);
|
|
4186
|
+
index++;
|
|
4187
|
+
}
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
return runDerive;
|
|
4191
|
+
}
|
|
4192
|
+
/**
|
|
4193
|
+
* The Node-direct twin of {@link buildFusedLifecycleWeb}: the same steps, finalizing through the
|
|
4194
|
+
* caller's own `finalize` (the Node outcome on the direct lane) instead of building a `Response`, so
|
|
4195
|
+
* the `!webFast` dispatcher runs these routes in one frame rather than the generic program. A
|
|
4196
|
+
* terminal `finalize` throw (an unstringifiable handler return on the Node lane) is caught as
|
|
4197
|
+
* `finishLifecycleContract` catches it; the body's 415/400 renders take `wrapResponse` directly.
|
|
4198
|
+
*/
|
|
4199
|
+
buildFusedLifecycleRunner(route, querySchema, bodySchema, maxBodyBytes) {
|
|
4200
|
+
const runDerive = sharedStages(NODE_STAGES, this, route, (d, b, a) => this.fusedNodeStages(d, b, a));
|
|
4201
|
+
const decorations = route.decorations;
|
|
4202
|
+
if (bodySchema !== undefined) {
|
|
4203
|
+
const onParsed = (parsed, ctx, finalize, wrapResponse) => {
|
|
3730
4204
|
let validation;
|
|
3731
4205
|
try {
|
|
3732
4206
|
validation = bodySchema["~standard"].validate(parsed);
|
|
3733
4207
|
}
|
|
3734
4208
|
catch (err) {
|
|
3735
|
-
return
|
|
4209
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
3736
4210
|
}
|
|
3737
4211
|
if (validation instanceof Promise) {
|
|
3738
4212
|
return validation.then((settled) => {
|
|
3739
4213
|
if (settled.issues !== undefined)
|
|
3740
|
-
return
|
|
4214
|
+
return wrapResponse(plainValidationError(settled.issues));
|
|
3741
4215
|
ctx.body = settled.value;
|
|
3742
|
-
return
|
|
3743
|
-
}, (err) =>
|
|
4216
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
4217
|
+
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
3744
4218
|
}
|
|
3745
4219
|
if (validation.issues !== undefined)
|
|
3746
|
-
return
|
|
4220
|
+
return wrapResponse(plainValidationError(validation.issues));
|
|
3747
4221
|
ctx.body = validation.value;
|
|
3748
|
-
return
|
|
4222
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
3749
4223
|
};
|
|
3750
|
-
return
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
* staged loops, per-stage dispatch, and the async-continuation machinery - while Bun rides the
|
|
3760
|
-
* single closure. That asymmetry is the realistic-route Node deficit against Fastify: this runner
|
|
3761
|
-
* closes it by running validate → derive → before → handler → (after) in one frame and finalizing
|
|
3762
|
-
* through the caller's own `finalize` (the Node outcome on the direct lane, `toResponse` on Web),
|
|
3763
|
-
* so the same registration feeds both runtimes with no per-lane copy of the lifecycle to drift.
|
|
3764
|
-
*
|
|
3765
|
-
* Semantics mirror the generic program stage-for-stage for the shapes this lane admits (exactly
|
|
3766
|
-
* one derive, one beforeHandle, zero-or-one afterHandle, no onError/around/decorations/contracts/
|
|
3767
|
-
* recovery hooks - enforced by `selectRouteLanes`): thrown `Response`/`status(...)` are control
|
|
3768
|
-
* flow through `wrapResponse`/`finalize`, anything else logs + 500s via the shared `bareError`
|
|
3769
|
-
* lane (identical to `handleLifecycleError` with no `onError` hooks), and a terminal `finalize`
|
|
3770
|
-
* throw (e.g. an unstringifiable handler return on the Node lane) is caught exactly as
|
|
3771
|
-
* `finishLifecycleContract` catches it. Step functions close over registration state only and take
|
|
3772
|
-
* the request state as arguments, so the hot path allocates no per-request closures.
|
|
3773
|
-
*/
|
|
3774
|
-
buildFusedDeriveBeforeRunner(handler, derive, before, after, querySchema, maxBodyBytes) {
|
|
3775
|
-
const logError = (err, ctx, finalize, wrapResponse) => this.bareError(err, ctx, finalize, wrapResponse);
|
|
3776
|
-
const runFinish = (result, ctx, finalize, wrapResponse) => {
|
|
3777
|
-
if (after === undefined) {
|
|
3778
|
-
try {
|
|
3779
|
-
return finalize(result, responseSet(ctx), ctx);
|
|
3780
|
-
}
|
|
3781
|
-
catch (err) {
|
|
3782
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3783
|
-
}
|
|
3784
|
-
}
|
|
3785
|
-
let transformed;
|
|
3786
|
-
try {
|
|
3787
|
-
transformed = after(result, ctx);
|
|
3788
|
-
}
|
|
3789
|
-
catch (err) {
|
|
3790
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3791
|
-
}
|
|
3792
|
-
if (transformed instanceof Promise) {
|
|
3793
|
-
return transformed.then((value) => {
|
|
3794
|
-
try {
|
|
3795
|
-
return finalize(value, responseSet(ctx), ctx);
|
|
3796
|
-
}
|
|
3797
|
-
catch (err) {
|
|
3798
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3799
|
-
}
|
|
3800
|
-
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3801
|
-
}
|
|
3802
|
-
try {
|
|
3803
|
-
return finalize(transformed, responseSet(ctx), ctx);
|
|
3804
|
-
}
|
|
3805
|
-
catch (err) {
|
|
3806
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3807
|
-
}
|
|
3808
|
-
};
|
|
3809
|
-
const finalizeEarly = (result, ctx, finalize, wrapResponse) => {
|
|
3810
|
-
try {
|
|
3811
|
-
return finalize(result, responseSet(ctx), ctx);
|
|
3812
|
-
}
|
|
3813
|
-
catch (err) {
|
|
3814
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3815
|
-
}
|
|
3816
|
-
};
|
|
3817
|
-
const runHandler = (ctx, finalize, wrapResponse) => {
|
|
3818
|
-
let result;
|
|
3819
|
-
try {
|
|
3820
|
-
result = handler(ctx);
|
|
3821
|
-
}
|
|
3822
|
-
catch (err) {
|
|
3823
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3824
|
-
}
|
|
3825
|
-
if (result instanceof Promise) {
|
|
3826
|
-
return result.then((value) => runFinish(value, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3827
|
-
}
|
|
3828
|
-
return runFinish(result, ctx, finalize, wrapResponse);
|
|
3829
|
-
};
|
|
3830
|
-
const runBefore = (ctx, finalize, wrapResponse) => {
|
|
3831
|
-
let early;
|
|
3832
|
-
try {
|
|
3833
|
-
early = before(ctx);
|
|
3834
|
-
}
|
|
3835
|
-
catch (err) {
|
|
3836
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3837
|
-
}
|
|
3838
|
-
if (early instanceof Promise) {
|
|
3839
|
-
return early.then((settled) => settled === undefined
|
|
3840
|
-
? runHandler(ctx, finalize, wrapResponse)
|
|
3841
|
-
: finalizeEarly(settled, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3842
|
-
}
|
|
3843
|
-
if (early === undefined)
|
|
3844
|
-
return runHandler(ctx, finalize, wrapResponse);
|
|
3845
|
-
return finalizeEarly(early, ctx, finalize, wrapResponse);
|
|
3846
|
-
};
|
|
3847
|
-
const runDerive = (ctx, finalize, wrapResponse) => {
|
|
3848
|
-
let derived;
|
|
3849
|
-
try {
|
|
3850
|
-
derived = derive(ctx);
|
|
3851
|
-
}
|
|
3852
|
-
catch (err) {
|
|
3853
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3854
|
-
}
|
|
3855
|
-
if (derived instanceof Promise) {
|
|
3856
|
-
return derived.then((settled) => {
|
|
3857
|
-
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3858
|
-
return finalizeEarly(settled, ctx, finalize, wrapResponse);
|
|
3859
|
-
}
|
|
3860
|
-
Object.assign(ctx, settled);
|
|
3861
|
-
return runBefore(ctx, finalize, wrapResponse);
|
|
3862
|
-
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3863
|
-
}
|
|
3864
|
-
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3865
|
-
return finalizeEarly(derived, ctx, finalize, wrapResponse);
|
|
3866
|
-
}
|
|
3867
|
-
Object.assign(ctx, derived);
|
|
3868
|
-
return runBefore(ctx, finalize, wrapResponse);
|
|
3869
|
-
};
|
|
4224
|
+
return (source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse) => {
|
|
4225
|
+
const ctx = nativeContext
|
|
4226
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
4227
|
+
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
4228
|
+
if (decorations !== undefined)
|
|
4229
|
+
Object.assign(ctx, decorations);
|
|
4230
|
+
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse), bodySchema, this.formOrigins);
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
3870
4233
|
const runValidated = (validation, ctx, finalize, wrapResponse) => {
|
|
3871
4234
|
if (validation instanceof Promise) {
|
|
3872
4235
|
return validation.then((settled) => {
|
|
3873
4236
|
if (settled.issues !== undefined)
|
|
3874
4237
|
return wrapResponse(plainValidationError(settled.issues));
|
|
3875
4238
|
ctx.query = settled.value;
|
|
3876
|
-
return runDerive(ctx, finalize, wrapResponse);
|
|
3877
|
-
}, (err) =>
|
|
4239
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
4240
|
+
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
3878
4241
|
}
|
|
3879
4242
|
if (validation.issues !== undefined)
|
|
3880
4243
|
return wrapResponse(plainValidationError(validation.issues));
|
|
3881
4244
|
ctx.query = validation.value;
|
|
3882
|
-
return runDerive(ctx, finalize, wrapResponse);
|
|
4245
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
3883
4246
|
};
|
|
3884
4247
|
return (source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse) => {
|
|
3885
4248
|
const ctx = nativeContext
|
|
3886
4249
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3887
4250
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
4251
|
+
if (decorations !== undefined)
|
|
4252
|
+
Object.assign(ctx, decorations);
|
|
3888
4253
|
if (querySchema === undefined)
|
|
3889
|
-
return runDerive(ctx, finalize, wrapResponse);
|
|
4254
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
3890
4255
|
let validation;
|
|
3891
4256
|
try {
|
|
3892
4257
|
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
|
|
3893
4258
|
}
|
|
3894
4259
|
catch (err) {
|
|
3895
|
-
return
|
|
4260
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
3896
4261
|
}
|
|
3897
4262
|
return runValidated(validation, ctx, finalize, wrapResponse);
|
|
3898
4263
|
};
|
|
3899
4264
|
}
|
|
3900
|
-
/**
|
|
3901
|
-
*
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3918
|
-
}
|
|
3919
|
-
}
|
|
3920
|
-
let transformed;
|
|
3921
|
-
try {
|
|
3922
|
-
transformed = after(result, ctx);
|
|
3923
|
-
}
|
|
3924
|
-
catch (err) {
|
|
3925
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3926
|
-
}
|
|
3927
|
-
if (transformed instanceof Promise) {
|
|
3928
|
-
return transformed.then((value) => {
|
|
3929
|
-
try {
|
|
3930
|
-
return finalize(value, responseSet(ctx), ctx);
|
|
3931
|
-
}
|
|
3932
|
-
catch (err) {
|
|
3933
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3934
|
-
}
|
|
3935
|
-
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3936
|
-
}
|
|
3937
|
-
try {
|
|
3938
|
-
return finalize(transformed, responseSet(ctx), ctx);
|
|
3939
|
-
}
|
|
3940
|
-
catch (err) {
|
|
3941
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3942
|
-
}
|
|
3943
|
-
};
|
|
3944
|
-
const finalizeEarly = (result, ctx, finalize, wrapResponse) => {
|
|
3945
|
-
try {
|
|
3946
|
-
return finalize(result, responseSet(ctx), ctx);
|
|
3947
|
-
}
|
|
3948
|
-
catch (err) {
|
|
3949
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3950
|
-
}
|
|
3951
|
-
};
|
|
3952
|
-
const runHandler = (ctx, finalize, wrapResponse) => {
|
|
3953
|
-
let result;
|
|
3954
|
-
try {
|
|
3955
|
-
result = handler(ctx);
|
|
3956
|
-
}
|
|
3957
|
-
catch (err) {
|
|
3958
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3959
|
-
}
|
|
3960
|
-
if (result instanceof Promise) {
|
|
3961
|
-
return result.then((value) => runFinish(value, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3962
|
-
}
|
|
3963
|
-
return runFinish(result, ctx, finalize, wrapResponse);
|
|
3964
|
-
};
|
|
3965
|
-
const runBefore = (ctx, finalize, wrapResponse) => {
|
|
3966
|
-
let early;
|
|
3967
|
-
try {
|
|
3968
|
-
early = before(ctx);
|
|
3969
|
-
}
|
|
3970
|
-
catch (err) {
|
|
3971
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3972
|
-
}
|
|
3973
|
-
if (early instanceof Promise) {
|
|
3974
|
-
return early.then((settled) => settled === undefined
|
|
3975
|
-
? runHandler(ctx, finalize, wrapResponse)
|
|
3976
|
-
: finalizeEarly(settled, ctx, finalize, wrapResponse), (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3977
|
-
}
|
|
3978
|
-
if (early === undefined)
|
|
3979
|
-
return runHandler(ctx, finalize, wrapResponse);
|
|
3980
|
-
return finalizeEarly(early, ctx, finalize, wrapResponse);
|
|
3981
|
-
};
|
|
3982
|
-
const runDerive = (ctx, finalize, wrapResponse) => {
|
|
3983
|
-
let derived;
|
|
3984
|
-
try {
|
|
3985
|
-
derived = derive(ctx);
|
|
3986
|
-
}
|
|
3987
|
-
catch (err) {
|
|
3988
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
3989
|
-
}
|
|
3990
|
-
if (derived instanceof Promise) {
|
|
3991
|
-
return derived.then((settled) => {
|
|
3992
|
-
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3993
|
-
return finalizeEarly(settled, ctx, finalize, wrapResponse);
|
|
3994
|
-
}
|
|
3995
|
-
Object.assign(ctx, settled);
|
|
3996
|
-
return runBefore(ctx, finalize, wrapResponse);
|
|
3997
|
-
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3998
|
-
}
|
|
3999
|
-
if (isResponseResult(derived) || derived instanceof Response) {
|
|
4000
|
-
return finalizeEarly(derived, ctx, finalize, wrapResponse);
|
|
4001
|
-
}
|
|
4002
|
-
Object.assign(ctx, derived);
|
|
4003
|
-
return runBefore(ctx, finalize, wrapResponse);
|
|
4004
|
-
};
|
|
4005
|
-
const onParsed = (parsed, ctx, finalize, wrapResponse) => {
|
|
4006
|
-
let validation;
|
|
4265
|
+
/** The thrown-value render of the Web fused lifecycle lanes for a route with no error hooks: a
|
|
4266
|
+
* `Response` is control flow, anything else is logged and answered with a plain 500. */
|
|
4267
|
+
fusedWebFail(err, ctx) {
|
|
4268
|
+
return this.bareError(err, ctx, this.finalizeResponse, this.wrapWebResponse);
|
|
4269
|
+
}
|
|
4270
|
+
/** {@link fusedWebFail} for a fused route that may have error hooks. */
|
|
4271
|
+
fusedWebRouteFail(err, ctx, route) {
|
|
4272
|
+
return this.fusedFail(err, ctx, route, this.finalizeResponse, this.wrapWebResponse);
|
|
4273
|
+
}
|
|
4274
|
+
/** The thrown-value render of a fused lifecycle route. A `Response` or `status(...)` is control flow
|
|
4275
|
+
* that no error hook sees, unless it cannot render, which is a fault like any other. Anything else
|
|
4276
|
+
* goes to the route's error hooks; with none it is logged and answered with a plain 500. */
|
|
4277
|
+
fusedFail(err, ctx, route, finalize, wrapResponse) {
|
|
4278
|
+
if (route.errors.length === 0)
|
|
4279
|
+
return this.bareError(err, ctx, finalize, wrapResponse);
|
|
4280
|
+
let fault = err;
|
|
4281
|
+
if (err instanceof Response || isResponseResult(err)) {
|
|
4007
4282
|
try {
|
|
4008
|
-
|
|
4009
|
-
}
|
|
4010
|
-
catch (err) {
|
|
4011
|
-
return logError(err, ctx, finalize, wrapResponse);
|
|
4283
|
+
return finalize(err, responseSet(ctx), ctx);
|
|
4012
4284
|
}
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
if (settled.issues !== undefined)
|
|
4016
|
-
return wrapResponse(plainValidationError(settled.issues));
|
|
4017
|
-
ctx.body = settled.value;
|
|
4018
|
-
return runDerive(ctx, finalize, wrapResponse);
|
|
4019
|
-
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
4285
|
+
catch (renderError) {
|
|
4286
|
+
fault = renderError;
|
|
4020
4287
|
}
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
return
|
|
4031
|
-
}
|
|
4288
|
+
}
|
|
4289
|
+
return this.runErrorHooks(route.errors, undefined, fault, ctx, (result, set) => finalize(result, set, ctx), wrapResponse);
|
|
4290
|
+
}
|
|
4291
|
+
/** {@link respondFused} for a fused lifecycle route, whose failures reach its error hooks. */
|
|
4292
|
+
respondHooks(result, ctx, route, unstamped) {
|
|
4293
|
+
try {
|
|
4294
|
+
return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, unstamped);
|
|
4295
|
+
}
|
|
4296
|
+
catch (err) {
|
|
4297
|
+
return this.fusedWebRouteFail(err, ctx, route);
|
|
4298
|
+
}
|
|
4032
4299
|
}
|
|
4033
4300
|
bareError(err, ctx, finalize, wrapResponse) {
|
|
4034
4301
|
// The thrown-value contract lives in `bare-error-lane.ts`, one source of truth for both this
|
|
@@ -4554,11 +4821,12 @@ export class Server {
|
|
|
4554
4821
|
}
|
|
4555
4822
|
/** Adapt one route's compiled execution plan to Bun's already-matched request shape. Route
|
|
4556
4823
|
* semantics remain in the plan; this closure only supplies native params and deadline fallback. */
|
|
4557
|
-
compileBunNativeHandler(entry, paramNames, fused, signal, budget, peer, fallback) {
|
|
4824
|
+
compileBunNativeHandler(entry, method, paramNames, fused, signal, budget, peer, fallback, hookedPlatform) {
|
|
4558
4825
|
// This callback is reached only from Bun's compiled native route table. Bun has already parsed
|
|
4559
4826
|
// the HTTP framing, so the JSON lane may retain its native fused `json()` parse without weakening
|
|
4560
|
-
// the portable/adapted source contract (those sources are never marked).
|
|
4561
|
-
|
|
4827
|
+
// the portable/adapted source contract (those sources are never marked). Bun hands a GET or
|
|
4828
|
+
// HEAD slot no body, so it skips the symbol write on every request.
|
|
4829
|
+
const markFramed = method === "GET" || method === "HEAD" ? () => undefined : markTrustedBodyFraming;
|
|
4562
4830
|
// The direct native lanes bypass `runMatched`, so the route's transport byte cap must be
|
|
4563
4831
|
// marked here too - otherwise a Bun `listen()` fused route would leave direct `c.req` body
|
|
4564
4832
|
// reads uncapped. The fallback branches go through `fetchMatched` -> `runMatched`, which marks.
|
|
@@ -4572,7 +4840,9 @@ export class Server {
|
|
|
4572
4840
|
(signal && budget
|
|
4573
4841
|
? (source, params, search, activeSignal, activeBudget, platform, nativeContext) => entry.execution.run(this, entry, source, params, search, activeSignal, activeBudget, platform, nativeContext, this.finalizeResponse, this.wrapWebResponse)
|
|
4574
4842
|
: undefined);
|
|
4575
|
-
|
|
4843
|
+
// The cap is never marked on a GET or HEAD, the only methods a GET or HEAD slot is handed, so those
|
|
4844
|
+
// slots skip the wrapper and its read of the method on every request.
|
|
4845
|
+
const capped = inner === undefined || bodyLimit === undefined || method === "GET" || method === "HEAD"
|
|
4576
4846
|
? inner
|
|
4577
4847
|
: (source, params, search, fusedSignal, fusedBudget, platform, nativeContext) => {
|
|
4578
4848
|
const method = source.method;
|
|
@@ -4600,6 +4870,12 @@ export class Server {
|
|
|
4600
4870
|
: finishNative(request, outcome);
|
|
4601
4871
|
};
|
|
4602
4872
|
if (paramNames.length === 0) {
|
|
4873
|
+
if (hookedPlatform !== undefined) {
|
|
4874
|
+
return (request, server) => {
|
|
4875
|
+
markFramed(request);
|
|
4876
|
+
return this.fetchMatchedHooked(request, entry, EMPTY_PARAMS, hookedPlatform(request, server));
|
|
4877
|
+
};
|
|
4878
|
+
}
|
|
4603
4879
|
if (capped === undefined) {
|
|
4604
4880
|
return (request) => {
|
|
4605
4881
|
markFramed(request);
|
|
@@ -4622,6 +4898,16 @@ export class Server {
|
|
|
4622
4898
|
const malformed = paramNames.length === 1
|
|
4623
4899
|
? (params) => unroutedParam(params[paramNames[0]])
|
|
4624
4900
|
: hasUnroutedParam;
|
|
4901
|
+
if (hookedPlatform !== undefined) {
|
|
4902
|
+
return (request, server) => {
|
|
4903
|
+
markFramed(request);
|
|
4904
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Bun's route table hands each handler a Request carrying the `params` it matched.
|
|
4905
|
+
const params = request.params ?? EMPTY_PARAMS;
|
|
4906
|
+
if (malformed(params))
|
|
4907
|
+
return fallback(request, server);
|
|
4908
|
+
return this.fetchMatchedHooked(request, entry, params, hookedPlatform(request, server));
|
|
4909
|
+
};
|
|
4910
|
+
}
|
|
4625
4911
|
if (capped === undefined) {
|
|
4626
4912
|
return (request, server) => {
|
|
4627
4913
|
markFramed(request);
|
|
@@ -4650,26 +4936,25 @@ export class Server {
|
|
|
4650
4936
|
return runNative(request, params);
|
|
4651
4937
|
};
|
|
4652
4938
|
}
|
|
4653
|
-
/** Compile portable route registrations into Bun's native route table.
|
|
4654
|
-
*
|
|
4655
|
-
* route
|
|
4939
|
+
/** Compile portable route registrations into Bun's native route table. Paths a composed mount or
|
|
4940
|
+
* a WebSocket handshake may reach stay with the portable dispatcher; provably disjoint HTTP routes
|
|
4941
|
+
* keep native matching. Request hooks run before the matched route and rewrites return to dispatch.
|
|
4656
4942
|
*
|
|
4657
4943
|
* Bun and the portable router agree on which of two static-and-`:param` paths wins a request, so
|
|
4658
4944
|
* the table is kept to those paths. Where Bun would otherwise pass a request on to a less specific
|
|
4659
4945
|
* path, the more specific one carries `fallback`, the portable dispatcher, under that method.
|
|
4660
4946
|
* `peer` is the platform every request served from the table shares. */
|
|
4661
|
-
buildBunNativeRoutes(fallback, peer) {
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
this.
|
|
4667
|
-
this.preRouteMountCount > 0 ||
|
|
4668
|
-
this.clientIpTrust !== undefined) {
|
|
4947
|
+
buildBunNativeRoutes(fallback, peer, platformFor) {
|
|
4948
|
+
const hooked = this.onRequestHooks.length > 0 && !this.bunNativeRequestHooksSafe;
|
|
4949
|
+
if ((hooked && platformFor === undefined) ||
|
|
4950
|
+
// A hook may rewrite a handshake onto a socket path, which only the dispatcher's upgrade check follows.
|
|
4951
|
+
(hooked && this.wsRoutes.length > 0) ||
|
|
4952
|
+
this.mountsTakeUpgrades()) {
|
|
4669
4953
|
return undefined;
|
|
4670
4954
|
}
|
|
4671
4955
|
const routes = Object.create(null);
|
|
4672
|
-
const mayUseFusedNative =
|
|
4956
|
+
const mayUseFusedNative = !hooked &&
|
|
4957
|
+
this.requestTimeoutMs === 0 &&
|
|
4673
4958
|
(this.onResponseHooks.length === 0 || this.bunNativeResponseHeadersOnly) &&
|
|
4674
4959
|
this.onResponseFinalizedHooks.length === 0 &&
|
|
4675
4960
|
// The capacity gate must wrap every request; the fused lane bypasses fetchMatched, so enabling
|
|
@@ -4679,28 +4964,49 @@ export class Server {
|
|
|
4679
4964
|
const unboundedBudget = mayUseFusedNative ? getUnboundedRequestBudget() : undefined;
|
|
4680
4965
|
let count = 0;
|
|
4681
4966
|
const all = this.catalog.entries();
|
|
4967
|
+
const preRouteMounts = this.fetchMounts.filter((mount) => mount.beforeRoutes);
|
|
4682
4968
|
// A path with a wildcard or a part-literal segment cannot go in the table: Bun reads `/:name.json`
|
|
4683
4969
|
// as one parameter named `name.json`. These are filed under each run of static segments they
|
|
4684
4970
|
// start with, which is where a table path they could outrank looks for them.
|
|
4685
4971
|
const under = new Map();
|
|
4972
|
+
const fileUnder = (segments) => {
|
|
4973
|
+
let prefix = "";
|
|
4974
|
+
for (const segment of segments) {
|
|
4975
|
+
let bucket = under.get(prefix);
|
|
4976
|
+
if (bucket === undefined) {
|
|
4977
|
+
bucket = [];
|
|
4978
|
+
under.set(prefix, bucket);
|
|
4979
|
+
}
|
|
4980
|
+
bucket.push(segments);
|
|
4981
|
+
if (segment.kind !== "static")
|
|
4982
|
+
break;
|
|
4983
|
+
prefix += `/${segment.value}`;
|
|
4984
|
+
}
|
|
4985
|
+
};
|
|
4986
|
+
// A handshake is a GET the portable dispatcher gives to a socket route before any HTTP route, so a
|
|
4987
|
+
// socket path is outside the table like a wildcard path, and a GET route a request may share with
|
|
4988
|
+
// one stays with the dispatcher.
|
|
4989
|
+
const sockets = this.wsRoutes.map((route) => compileRoutePattern(route.path).segments);
|
|
4990
|
+
for (const segments of sockets)
|
|
4991
|
+
fileUnder(segments);
|
|
4686
4992
|
const lead = all.map((route) => {
|
|
4687
4993
|
const segments = route.pattern.segments;
|
|
4688
4994
|
const plain = segments.every((segment) => segment.kind === "static" || segment.kind === "param");
|
|
4995
|
+
if (!plain)
|
|
4996
|
+
fileUnder(segments);
|
|
4689
4997
|
let prefix = "";
|
|
4690
4998
|
for (const segment of segments) {
|
|
4691
|
-
if (!plain) {
|
|
4692
|
-
let bucket = under.get(prefix);
|
|
4693
|
-
if (bucket === undefined) {
|
|
4694
|
-
bucket = [];
|
|
4695
|
-
under.set(prefix, bucket);
|
|
4696
|
-
}
|
|
4697
|
-
bucket.push(route);
|
|
4698
|
-
}
|
|
4699
4999
|
if (segment.kind !== "static")
|
|
4700
5000
|
break;
|
|
4701
5001
|
prefix += `/${segment.value}`;
|
|
4702
5002
|
}
|
|
4703
|
-
|
|
5003
|
+
// Mounts take every method under their literal prefix before route matching. A parameter may
|
|
5004
|
+
// fill the rest of that prefix, but a static route ending above the mount cannot reach it.
|
|
5005
|
+
// Conservative by design: only the leading static run proves a dynamic route disjoint.
|
|
5006
|
+
const mayHitMount = preRouteMounts.some((mount) => underMountPrefix(prefix, mount.path) ||
|
|
5007
|
+
(route.pattern.paramNames.length > 0 &&
|
|
5008
|
+
(prefix === "" || underMountPrefix(mount.path, prefix))));
|
|
5009
|
+
return plain && !mayHitMount ? prefix : undefined;
|
|
4704
5010
|
});
|
|
4705
5011
|
// A preflight hook is intentionally handled by the fallback fetch path. Keeping OPTIONS out
|
|
4706
5012
|
// of the native lane means Bun dispatches it through CORS's onRequest hook even when the
|
|
@@ -4726,14 +5032,21 @@ export class Server {
|
|
|
4726
5032
|
entry.idempotent !== undefined ||
|
|
4727
5033
|
!BUN_TABLE_METHODS.includes(method) ||
|
|
4728
5034
|
(preflightOnFallback && method === "OPTIONS") ||
|
|
4729
|
-
under.get(run)?.some((other) => outranks(other
|
|
5035
|
+
under.get(run)?.some((other) => outranks(other, pattern.segments)) ||
|
|
5036
|
+
(method === "GET" && sockets.some((socket) => mayShareRequest(socket, pattern.segments)))) {
|
|
4730
5037
|
continue;
|
|
4731
5038
|
}
|
|
4732
5039
|
const paramNames = pattern.paramNames;
|
|
4733
5040
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined;
|
|
4734
|
-
const handler = this.compileBunNativeHandler(entry, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback);
|
|
4735
|
-
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it.
|
|
4736
|
-
|
|
5041
|
+
const handler = this.compileBunNativeHandler(entry, method, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback, hooked ? platformFor : undefined);
|
|
5042
|
+
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it. Left
|
|
5043
|
+
// off, `validateHost` costs nothing here: the table holds no wildcard, a `..` parameter falls
|
|
5044
|
+
// back, and reading the Host on every request measured about 6% of a bare route.
|
|
5045
|
+
tableAt(path, pattern.segments)[method] = this.validateHost
|
|
5046
|
+
? (request, server) => requestTarget(request, true, this.normalizeTarget)
|
|
5047
|
+
? fallback(request, server)
|
|
5048
|
+
: handler(request, server)
|
|
5049
|
+
: handler;
|
|
4737
5050
|
count += 1;
|
|
4738
5051
|
if (paramNames.length > 0) {
|
|
4739
5052
|
below.add(pattern.segments.length + method);
|
|
@@ -4787,11 +5100,17 @@ export class Server {
|
|
|
4787
5100
|
* nothing - so any endpoint slower than 10s (a render, an export, a long upstream call) has its
|
|
4788
5101
|
* connection cut mid-flight regardless of `requestTimeoutMs`. Apps with such endpoints must raise
|
|
4789
5102
|
* this above their slowest expected response, which is why it is a first-class option and not a
|
|
4790
|
-
* reason to drop down to `Bun.serve
|
|
5103
|
+
* reason to drop down to `Bun.serve`, or hold those requests' connections alone with
|
|
5104
|
+
* `withIdleTimeout()` from `@nifrajs/core/idle-timeout`. `0` disables the timeout entirely; max 255.
|
|
4791
5105
|
*
|
|
4792
5106
|
* `tls` serves HTTPS directly: `app.listen(443, { tls: { cert, key } })` with PEM text or bytes.
|
|
4793
5107
|
* Requests then arrive with `https:` URLs. Behind a proxy or a platform that terminates TLS, leave
|
|
4794
5108
|
* it unset.
|
|
5109
|
+
*
|
|
5110
|
+
* `static` sees every request before the app does, the ones Bun's route table serves included, so
|
|
5111
|
+
* a file it answers wins over any route at the same path. It returns the file's response, or
|
|
5112
|
+
* `undefined` to let the app route the request. No hook of the app runs for a request it answers.
|
|
5113
|
+
* `@nifrajs/web`'s Bun build serves its client bundle and public files this way.
|
|
4795
5114
|
*/
|
|
4796
5115
|
listen(port, options) {
|
|
4797
5116
|
if (typeof Bun === "undefined") {
|
|
@@ -4844,6 +5163,10 @@ export class Server {
|
|
|
4844
5163
|
return jsonError(500, "internal_error");
|
|
4845
5164
|
},
|
|
4846
5165
|
};
|
|
5166
|
+
const idleTimeoutSec = options?.idleTimeoutSec ?? BUN_IDLE_TIMEOUT_SEC;
|
|
5167
|
+
const setIdleTimeout = (request, seconds) =>
|
|
5168
|
+
// 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`.
|
|
5169
|
+
running.timeout(request, seconds ?? idleTimeoutSec);
|
|
4847
5170
|
const fallback = (req, server) => {
|
|
4848
5171
|
// Bun has already framed and bounded this request body in its HTTP parser. Mark the
|
|
4849
5172
|
// source before the portable fallback runs so body schemas use Bun's native `json()`
|
|
@@ -4851,25 +5174,59 @@ export class Server {
|
|
|
4851
5174
|
// Requests. Native route handlers mark themselves in compileBunNativeHandler; this
|
|
4852
5175
|
// covers every request the native lane does not serve.
|
|
4853
5176
|
markTrustedBodyFraming(req);
|
|
4854
|
-
return this.fetch(req,
|
|
5177
|
+
return this.fetch(req, new BunPeerPlatform(server, req, setIdleTimeout));
|
|
4855
5178
|
};
|
|
4856
5179
|
// Bun names the socket peer of a request when its server is asked. A request served from the
|
|
4857
5180
|
// native table carries this one platform, which asks only when a handler reads `c.clientIp`.
|
|
5181
|
+
const socketPeer = (request) => running.requestIP(request)?.address;
|
|
5182
|
+
// A `clientIp` trust declaration resolves here too, from the request's headers, when a handler reads
|
|
5183
|
+
// the address; only a declaration of zero hops names the socket peer.
|
|
5184
|
+
const trust = this.clientIpTrust;
|
|
4858
5185
|
const peer = {
|
|
4859
|
-
[PLATFORM_PEER]:
|
|
5186
|
+
[PLATFORM_PEER]: trust === undefined
|
|
5187
|
+
? socketPeer
|
|
5188
|
+
: (request) => resolveClientIp(!("header" in trust) && trust.trustedHops === 0 ? socketPeer(request) : undefined,
|
|
5189
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: every source a Bun listener routes is a `Request`.
|
|
5190
|
+
request, trust),
|
|
5191
|
+
[PLATFORM_IDLE_TIMEOUT]: setIdleTimeout,
|
|
4860
5192
|
};
|
|
4861
|
-
const nativeRoutes =
|
|
5193
|
+
const nativeRoutes = this.buildBunNativeRoutes(fallback, peer, (request, server) =>
|
|
5194
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: `fallback` hands `fetch` the same platform, which holds no environment.
|
|
5195
|
+
new BunPeerPlatform(server, request, setIdleTimeout));
|
|
5196
|
+
const first = options?.static;
|
|
5197
|
+
const before = (inner) => first === undefined
|
|
5198
|
+
? inner
|
|
5199
|
+
: (req, server) => {
|
|
5200
|
+
const answer = first(req);
|
|
5201
|
+
if (answer === undefined)
|
|
5202
|
+
return inner(req, server);
|
|
5203
|
+
return answer instanceof Promise
|
|
5204
|
+
? answer.then((response) => response ?? inner(req, server))
|
|
5205
|
+
: answer;
|
|
5206
|
+
};
|
|
5207
|
+
// Bun runs a table handler without `fetch`, so each one gets the same front as the fallback.
|
|
5208
|
+
if (first !== undefined && nativeRoutes !== undefined) {
|
|
5209
|
+
for (const path in nativeRoutes) {
|
|
5210
|
+
const methods = nativeRoutes[path];
|
|
5211
|
+
for (const method of BUN_TABLE_METHODS) {
|
|
5212
|
+
const handler = methods[method];
|
|
5213
|
+
if (handler !== undefined)
|
|
5214
|
+
methods[method] = before(handler);
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
4862
5218
|
const sockets = new Set();
|
|
4863
5219
|
this.bunSockets = sockets;
|
|
4864
5220
|
const running = (wsHandlers === undefined
|
|
4865
5221
|
? Bun.serve({
|
|
4866
5222
|
...listening,
|
|
4867
5223
|
...(nativeRoutes === undefined ? {} : { routes: nativeRoutes }),
|
|
4868
|
-
fetch: fallback,
|
|
5224
|
+
fetch: before(fallback),
|
|
4869
5225
|
})
|
|
4870
5226
|
: Bun.serve({
|
|
4871
5227
|
...listening,
|
|
4872
|
-
|
|
5228
|
+
...(nativeRoutes === undefined ? {} : { routes: nativeRoutes }),
|
|
5229
|
+
fetch: before((req, server) => this.bunFetchWithWebSocket(req, server, setIdleTimeout)),
|
|
4873
5230
|
// Bun's `ServerWebSocket<BunWsData>` is runtime-compatible with the handlers' structural
|
|
4874
5231
|
// `BunSocket` view (kept local so `Bun.*` types never leak into the published .d.ts); the
|
|
4875
5232
|
// `unknown` params bridge a TS structural-variance quirk. Round-trip covered by websocket.test.ts.
|