@nifrajs/core 2.14.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/assurance.d.ts +16 -8
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +33 -0
- package/dist/assurance.js.map +1 -1
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +26 -3
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-approval.d.ts +56 -0
- package/dist/durable-approval.d.ts.map +1 -0
- package/dist/durable-approval.js +233 -0
- package/dist/durable-approval.js.map +1 -0
- package/dist/durable-effect.d.ts +42 -0
- package/dist/durable-effect.d.ts.map +1 -0
- package/dist/durable-effect.js +234 -0
- package/dist/durable-effect.js.map +1 -0
- package/dist/durable-execution.d.ts +8 -346
- package/dist/durable-execution.d.ts.map +1 -1
- package/dist/durable-execution.js +7 -1150
- package/dist/durable-execution.js.map +1 -1
- package/dist/durable-saga.d.ts +94 -0
- package/dist/durable-saga.d.ts.map +1 -0
- package/dist/durable-saga.js +550 -0
- package/dist/durable-saga.js.map +1 -0
- package/dist/edge-kit.d.ts +27 -0
- package/dist/edge-kit.d.ts.map +1 -0
- package/dist/edge-kit.js +24 -0
- package/dist/edge-kit.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +23 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/durable-approval-protocol.d.ts +1 -1
- package/dist/internal/durable-approval-protocol.d.ts.map +1 -1
- package/dist/internal/durable-effect-protocol.d.ts +1 -1
- package/dist/internal/durable-effect-protocol.d.ts.map +1 -1
- package/dist/internal/durable-saga-protocol.d.ts +1 -1
- package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
- package/dist/internal/durable-shared.d.ts +26 -0
- package/dist/internal/durable-shared.d.ts.map +1 -0
- package/dist/internal/durable-shared.js +131 -0
- package/dist/internal/durable-shared.js.map +1 -0
- package/dist/internal/durable-types.d.ts +159 -0
- package/dist/internal/durable-types.d.ts.map +1 -0
- package/dist/internal/durable-types.js +2 -0
- package/dist/internal/durable-types.js.map +1 -0
- package/dist/internal/route-compiler.d.ts +61 -0
- package/dist/internal/route-compiler.d.ts.map +1 -0
- package/dist/internal/route-compiler.js +121 -0
- package/dist/internal/route-compiler.js.map +1 -0
- package/dist/internal/route-execution.d.ts +17 -9
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +4 -18
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +35 -0
- package/dist/internal/route-lanes.d.ts.map +1 -0
- package/dist/internal/route-lanes.js +150 -0
- package/dist/internal/route-lanes.js.map +1 -0
- package/dist/internal/route-program.d.ts +67 -0
- package/dist/internal/route-program.d.ts.map +1 -0
- package/dist/internal/route-program.js +199 -0
- package/dist/internal/route-program.js.map +1 -0
- package/dist/response-observer.d.ts +25 -0
- package/dist/response-observer.d.ts.map +1 -0
- package/dist/response-observer.js +40 -0
- package/dist/response-observer.js.map +1 -0
- package/dist/security/baseline.d.ts +68 -0
- package/dist/security/baseline.d.ts.map +1 -0
- package/dist/security/baseline.js +118 -0
- package/dist/security/baseline.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +26 -0
- package/dist/server/bare-error-lane.d.ts.map +1 -0
- package/dist/server/bare-error-lane.js +60 -0
- package/dist/server/bare-error-lane.js.map +1 -0
- package/dist/server/body-lane.d.ts +13 -0
- package/dist/server/body-lane.d.ts.map +1 -0
- package/dist/server/body-lane.js +134 -0
- package/dist/server/body-lane.js.map +1 -0
- package/dist/server/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +7 -0
- package/dist/server/body.js.map +1 -1
- package/dist/server/bridge-markers.d.ts +25 -0
- package/dist/server/bridge-markers.d.ts.map +1 -0
- package/dist/server/bridge-markers.js +25 -0
- package/dist/server/bridge-markers.js.map +1 -0
- package/dist/server/context.d.ts +6 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/contract.d.ts +40 -14
- package/dist/server/contract.d.ts.map +1 -1
- package/dist/server/contract.js +7 -0
- package/dist/server/contract.js.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +16 -2
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/header-case.d.ts.map +1 -1
- package/dist/server/header-case.js +2 -1
- package/dist/server/header-case.js.map +1 -1
- package/dist/server/http.d.ts +36 -3
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +49 -3
- package/dist/server/http.js.map +1 -1
- package/dist/server/install.d.ts +6 -8
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +7 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/node-outcome-hook.d.ts +2 -1
- package/dist/server/node-outcome-hook.d.ts.map +1 -1
- package/dist/server/node-outcome-hook.js.map +1 -1
- package/dist/server/node-outcome.d.ts +17 -2
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +37 -3
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/plugin.d.ts +2 -2
- package/dist/server/plugin.d.ts.map +1 -1
- package/dist/server/plugin.js +6 -1
- package/dist/server/plugin.js.map +1 -1
- package/dist/server/query.d.ts +7 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +8 -3
- package/dist/server/query.js.map +1 -1
- package/dist/server/registry.d.ts +40 -12
- package/dist/server/registry.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +2 -13
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +18 -159
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +58 -4
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +5 -2
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts +20 -0
- package/dist/server/response-observer-runtime.d.ts.map +1 -0
- package/dist/server/response-observer-runtime.js +95 -0
- package/dist/server/response-observer-runtime.js.map +1 -0
- package/dist/server/runtime-core.d.ts +76 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +42 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +6 -1
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +158 -116
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +707 -844
- package/dist/server/server.js.map +1 -1
- package/dist/server/validation.d.ts +5 -0
- package/dist/server/validation.d.ts.map +1 -0
- package/dist/server/validation.js +13 -0
- package/dist/server/validation.js.map +1 -0
- package/dist/server/websocket.d.ts +20 -4
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +81 -11
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +12 -4
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +50 -16
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +3 -2
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy-register.d.ts +2 -0
- package/dist/single-copy-register.d.ts.map +1 -0
- package/dist/single-copy-register.js +24 -0
- package/dist/single-copy-register.js.map +1 -0
- package/dist/single-copy.d.ts +156 -0
- package/dist/single-copy.d.ts.map +1 -0
- package/dist/single-copy.js +472 -0
- package/dist/single-copy.js.map +1 -0
- package/package.json +28 -3
- package/src/assurance.ts +58 -1
- package/src/durable-adapters.ts +20 -0
- package/src/durable-approval.ts +281 -0
- package/src/durable-effect.ts +300 -0
- package/src/durable-execution.ts +8 -1627
- package/src/durable-saga.ts +744 -0
- package/src/edge-kit.ts +27 -0
- package/src/errors.ts +1 -0
- package/src/index.ts +129 -2
- package/src/internal/durable-approval-protocol.ts +1 -1
- package/src/internal/durable-effect-protocol.ts +1 -1
- package/src/internal/durable-saga-protocol.ts +1 -1
- package/src/internal/durable-shared.ts +157 -0
- package/src/internal/durable-types.ts +194 -0
- package/src/internal/route-compiler.ts +234 -0
- package/src/internal/route-execution.ts +53 -96
- package/src/internal/route-lanes.ts +221 -0
- package/src/internal/route-program.ts +404 -0
- package/src/response-observer.ts +64 -0
- package/src/security/baseline.ts +172 -0
- package/src/server/bare-error-lane.ts +77 -0
- package/src/server/body-lane.ts +203 -0
- package/src/server/body.ts +8 -0
- package/src/server/bridge-markers.ts +24 -0
- package/src/server/context.ts +6 -1
- package/src/server/contract.ts +70 -20
- package/src/server/cookies.ts +16 -2
- package/src/server/header-case.ts +3 -1
- package/src/server/http.ts +53 -3
- package/src/server/install.ts +14 -0
- package/src/server/node-outcome-hook.ts +2 -1
- package/src/server/node-outcome.ts +44 -3
- package/src/server/plugin.ts +11 -5
- package/src/server/query.ts +10 -4
- package/src/server/registry.ts +71 -16
- package/src/server/request-context.ts +13 -216
- package/src/server/respond.ts +71 -3
- package/src/server/response-contract-lane.ts +6 -2
- package/src/server/response-observer-runtime.ts +135 -0
- package/src/server/runtime-core.ts +124 -1
- package/src/server/server-types.ts +6 -1
- package/src/server/server.ts +1446 -1485
- package/src/server/validation.ts +19 -0
- package/src/server/websocket.ts +127 -17
- package/src/server/ws-bun.ts +67 -15
- package/src/server/ws-hook.ts +3 -2
- package/src/server.ts +8 -2
- package/src/single-copy-register.ts +24 -0
- package/src/single-copy.ts +526 -0
package/dist/server/server.js
CHANGED
|
@@ -1,113 +1,28 @@
|
|
|
1
1
|
import { admitDeadline, createRequestBudget, createUnboundedRequestBudget, NIFRA_DEADLINE_HEADER, } from "../budget.js";
|
|
2
2
|
import { FrameworkError, RouteConfigError } from "../errors.js";
|
|
3
|
-
import {
|
|
4
|
-
import { assuranceDeclarationsOf, assuranceEvidenceFor,
|
|
3
|
+
import { DEFAULT_CAPABILITY_INTERCEPTOR_TIMEOUT_MS, } from "../internal/capability-runtime.js";
|
|
4
|
+
import { assuranceDeclarationsOf, assuranceEvidenceFor, } from "../internal/route-assurance.js";
|
|
5
5
|
import { RouteCatalog } from "../internal/route-catalog.js";
|
|
6
|
+
import { compileRouteOptions } from "../internal/route-compiler.js";
|
|
6
7
|
import { compileRouteExecutionPlan, } from "../internal/route-execution.js";
|
|
8
|
+
import { compileRouteProgram, executeRouteProgram } from "../internal/route-program.js";
|
|
7
9
|
import { isSameOriginRequest } from "../internal/same-origin.js";
|
|
8
|
-
import {
|
|
10
|
+
import { decodeRouteParams } from "../router/pattern.js";
|
|
9
11
|
import { EMPTY_PARAMS, Router } from "../router/router.js";
|
|
10
|
-
import {
|
|
12
|
+
import { emitRequestErrorLog, renderBareError } from "./bare-error-lane.js";
|
|
13
|
+
import { assertByteLimit, markTransportCap, markTrustedBodyFraming, UNLIMITED_BODY_BYTES, } from "./body.js";
|
|
11
14
|
import { resolveClientIp } from "./client-ip.js";
|
|
12
15
|
import { hasLowercaseHeaderKeysMark, headerKeysAllLowercase, markLowercaseHeaderKeys, } from "./header-case.js";
|
|
13
16
|
import { headerObjectOf } from "./headers.js";
|
|
14
|
-
import { jsonError, pathnameOf, urlPartsOf } from "./http.js";
|
|
17
|
+
import { jsonError, pathnameOf, plainError, urlPartsOf } from "./http.js";
|
|
15
18
|
import { withStaticNodeHeaders } from "./node-outcome.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { applyBodyReplacement, withReplacedBody } from "./response-hooks.js";
|
|
21
|
-
import { CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, requestOf, } from "./runtime-core.js";
|
|
19
|
+
import { queryObjectOf, searchOf } from "./query.js";
|
|
20
|
+
import { RequestContext, readBodyFramed } from "./request-context.js";
|
|
21
|
+
import { applyStaticResponseHeaders, buildStaticResponseHeaders, fusedRespond, fusedRespondNoSet, toResponse, } from "./respond.js";
|
|
22
|
+
import { CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, } from "./runtime-core.js";
|
|
22
23
|
import { normalizeStaticResponseHeaders } from "./static-headers.js";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Detect a guarded Web Headers object before invoking user code. Retrying after catching any
|
|
27
|
-
* TypeError is observably wrong: user hooks are allowed to throw TypeError themselves, and a retry
|
|
28
|
-
* runs those hooks twice. The hot path never reaches the probe: every framework-constructed
|
|
29
|
-
* Response stamps its headers as known-mutable at construction (see respond.ts), so only a
|
|
30
|
-
* handler-returned foreign `Response` - the case that can actually be guarded - pays it, once per
|
|
31
|
-
* headers object. The probe uses a valid private header name and restores a pre-existing value, so
|
|
32
|
-
* the fallback is limited to the actual immutable/guarded-header case.
|
|
33
|
-
*/
|
|
34
|
-
function hasMutableResponseHeaders(headers) {
|
|
35
|
-
if (knownMutableHeaders(headers))
|
|
36
|
-
return true;
|
|
37
|
-
if (GUARDED_RESPONSE_HEADERS.has(headers))
|
|
38
|
-
return false;
|
|
39
|
-
let previous = null;
|
|
40
|
-
try {
|
|
41
|
-
previous = headers.get(HEADER_MUTABILITY_PROBE);
|
|
42
|
-
headers.set(HEADER_MUTABILITY_PROBE, "1");
|
|
43
|
-
if (previous === null)
|
|
44
|
-
headers.delete(HEADER_MUTABILITY_PROBE);
|
|
45
|
-
else
|
|
46
|
-
headers.set(HEADER_MUTABILITY_PROBE, previous);
|
|
47
|
-
rememberMutableHeaders(headers);
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
try {
|
|
52
|
-
if (previous === null)
|
|
53
|
-
headers.delete(HEADER_MUTABILITY_PROBE);
|
|
54
|
-
else
|
|
55
|
-
headers.set(HEADER_MUTABILITY_PROBE, previous);
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
// The guarded object rejected the cleanup too; the clone below is authoritative.
|
|
59
|
-
}
|
|
60
|
-
GUARDED_RESPONSE_HEADERS.add(headers);
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/** Adapt a portable {@link ResponseBodyHook} into the Web `onResponse` walk. Only a Response
|
|
65
|
-
* carrying the framework-buffered body tag participates - a raw or streamed Response is skipped by
|
|
66
|
-
* contract, never drained. */
|
|
67
|
-
function webResponseBodyHook(fn, owners) {
|
|
68
|
-
return (response, req) => {
|
|
69
|
-
const body = taggedResponseBody(response, owners);
|
|
70
|
-
if (body === undefined)
|
|
71
|
-
return response;
|
|
72
|
-
const out = fn(body, response.headers, webRequestView(req), response.status);
|
|
73
|
-
if (out instanceof Promise)
|
|
74
|
-
return out.then((replaced) => withReplacedBody(response, replaced));
|
|
75
|
-
return withReplacedBody(response, out);
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
/** Minimal request view over a Web `Request` for portable header hooks on the Web serving paths. */
|
|
79
|
-
function webRequestView(req) {
|
|
80
|
-
return { method: req.method, url: req.url, header: (name) => req.headers.get(name) };
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Adapt a portable {@link ResponseHeadersHook} into the Web `onResponse` walk: run it against the
|
|
84
|
-
* response's own `Headers` in place (no clone). A response whose headers are GUARDED (a raw
|
|
85
|
-
* `fetch()`ed Response returned by a handler) throws on the FIRST mutation, so nothing was applied
|
|
86
|
-
* yet - rerun once against a mutable copy. Async hooks should perform their mutations before their
|
|
87
|
-
* first await for the guard fallback to cover them.
|
|
88
|
-
*/
|
|
89
|
-
function webResponseHeadersHook(fn) {
|
|
90
|
-
return (response, req) => {
|
|
91
|
-
const view = webRequestView(req);
|
|
92
|
-
if (!hasMutableResponseHeaders(response.headers)) {
|
|
93
|
-
const clone = new Response(response.body, response);
|
|
94
|
-
const out = fn(clone.headers, view, clone.status);
|
|
95
|
-
return out instanceof Promise ? out.then(() => clone) : clone;
|
|
96
|
-
}
|
|
97
|
-
const out = fn(response.headers, view, response.status);
|
|
98
|
-
return out instanceof Promise ? out.then(() => response) : response;
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
/** Adapt a raw-response fallback hook. Tagged framework payloads already ran through the body tier,
|
|
102
|
-
* while untagged Responses include streams, proxied fetches, and framework-generated error responses. */
|
|
103
|
-
function webResponseRawHook(fn, owners) {
|
|
104
|
-
return (response, req) => {
|
|
105
|
-
if (taggedResponseBody(response, owners) !== undefined)
|
|
106
|
-
return response;
|
|
107
|
-
return fn(response, req);
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
import { INSTALL_EFFECT_LEDGER, INSTALL_IDEMPOTENCY, INSTALL_MCP, INSTALL_NODE_DIRECT, INSTALL_RESPONSE_CONTRACT, INSTALL_SSE, INSTALL_WS, } from "./install.js";
|
|
24
|
+
import { plainValidationError } from "./validation.js";
|
|
25
|
+
import { INSTALL_EFFECT_LEDGER, INSTALL_IDEMPOTENCY, INSTALL_MCP, INSTALL_NODE_DIRECT, INSTALL_RESPONSE_CONTRACT, INSTALL_RESPONSE_OBSERVER, INSTALL_SSE, INSTALL_WS, NODE_NATIVE_MOUNT, RESOLVE_NODE_MOUNT, } from "./install.js";
|
|
111
26
|
import { jsonLogger } from "./logger.js";
|
|
112
27
|
const functionToString = Function.prototype.toString;
|
|
113
28
|
const CONTEXTLESS_ARROW = /^(?:async\s*)?\(\s*\)\s*(?::[\s\S]*?)?=>/;
|
|
@@ -162,16 +77,13 @@ const STOP_HOOK_TIMEOUT_MS = 5_000;
|
|
|
162
77
|
* owner, shared with the server-function mount in `@nifrajs/web` - the two used to answer differently
|
|
163
78
|
* for the same request, so a browser that could open a socket was told its POST was cross-origin. */
|
|
164
79
|
const wsSameOrigin = isSameOriginRequest;
|
|
165
|
-
function validationError(issues) {
|
|
166
|
-
const serialized = issues.map((issue) => {
|
|
167
|
-
const path = issue.path?.map((seg) => String(typeof seg === "object" ? seg.key : seg));
|
|
168
|
-
return path !== undefined ? { message: issue.message, path } : { message: issue.message };
|
|
169
|
-
});
|
|
170
|
-
return Response.json({ ok: false, error: "validation", issues: serialized }, { status: 422 });
|
|
171
|
-
}
|
|
172
80
|
// `jsonError`, `urlPartsOf`, `pathnameOf` moved to `./http.ts` (a dependency-free leaf shared with the
|
|
173
81
|
// opt-in request lanes); re-exported so existing importers keep resolving from here.
|
|
174
82
|
export { pathnameOf, urlPartsOf } from "./http.js";
|
|
83
|
+
/** The 422 as plain data - the shape every lane's response wrapper takes, so a rejected body costs
|
|
84
|
+
* what an accepted one costs: on Node it is written straight to the socket with a `content-length`
|
|
85
|
+
* instead of being built as a `Response` and drained back out. */
|
|
86
|
+
export { plainValidationError } from "./validation.js";
|
|
175
87
|
// Query-string + urlencoded-form parsing now lives in `./query.ts`; re-exported so existing
|
|
176
88
|
// importers keep resolving `searchOf`/`queryObjectOf`/`QueryValue` from here.
|
|
177
89
|
export { queryObjectOf, searchOf };
|
|
@@ -220,8 +132,15 @@ function isContextlessNoArgArrow(handler) {
|
|
|
220
132
|
return false;
|
|
221
133
|
}
|
|
222
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The Web lanes' `wrapResponse` input, as a `Response`. A plain-data carrier ({@link plainError},
|
|
137
|
+
* `status(...)`) is rendered on the ordinary JSON lane rather than through its own `toResponse`, so
|
|
138
|
+
* it picks up the same prebuilt init a handler's plain return uses. `EMPTY_RESPONSE_CONTROLS` because
|
|
139
|
+
* these renders happen where no request context exists - before routing, or after it was abandoned.
|
|
140
|
+
*/
|
|
141
|
+
const webResponseOf = (result) => result instanceof Response ? result : toResponse(result, EMPTY_RESPONSE_CONTROLS);
|
|
223
142
|
// Stable module-level finalizers so `fetch`/`resolveNode` allocate no per-request closures.
|
|
224
|
-
const IDENTITY_RESPONSE = (response) => response;
|
|
143
|
+
const IDENTITY_RESPONSE = (response) => webResponseOf(response);
|
|
225
144
|
const RESPONSE_TIMEOUT = () => jsonError(503, "request_timeout");
|
|
226
145
|
// The unused-order-scoped-hook audit (recorder, seal check, stack capture, message) is a dev-time
|
|
227
146
|
// diagnostic. Every guard below keeps the INLINE literal `process.env.NODE_ENV !== "production"` at the
|
|
@@ -316,6 +235,13 @@ export class Server {
|
|
|
316
235
|
* Created by the first `app.ws()` via the `@nifrajs/core/ws` runtime - `undefined` until then, so a
|
|
317
236
|
* no-WebSocket app never constructs (or bundles) it. */
|
|
318
237
|
topics;
|
|
238
|
+
/** True once any `app.ws()` route sets `validateSend: true` - the one case where a broadcast frame
|
|
239
|
+
* is validated (and possibly dropped) per socket, so `app.publish` must stay on the JS registry loop
|
|
240
|
+
* rather than delegate to Bun's native `server.publish`. */
|
|
241
|
+
wsHasValidatedSend = false;
|
|
242
|
+
/** Bun's native topic broadcast, bound by `listen()` in native-pubsub mode; `app.publish` uses it
|
|
243
|
+
* instead of the JS registry. `undefined` off Bun, before `listen()`, or with a validated-send route. */
|
|
244
|
+
nativePublish;
|
|
319
245
|
maxBodyBytes;
|
|
320
246
|
protoPoisoning;
|
|
321
247
|
/** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
|
|
@@ -525,6 +451,11 @@ export class Server {
|
|
|
525
451
|
* declared *after* this call, and reaches none declared before. One added after the last route
|
|
526
452
|
* (e.g. on the app returned by a route-registering factory) covers nothing - see
|
|
527
453
|
* {@link ServerOptions.unusedScopedHooks}.
|
|
454
|
+
*
|
|
455
|
+
* Returning a `status(...)` (or a `Response`) instead of an extension ends the request there - the
|
|
456
|
+
* guard shape, and the cheap one: it renders as plain data on the same lane a handler's return
|
|
457
|
+
* takes, where `throw new Response(...)` pays ~40% of the rejection lane to build the object. That
|
|
458
|
+
* branch is control flow, so it does not widen the derived context type.
|
|
528
459
|
*/
|
|
529
460
|
derive(fn) {
|
|
530
461
|
this.assertConfigurable("derive()");
|
|
@@ -671,19 +602,33 @@ export class Server {
|
|
|
671
602
|
* hook owns part of the header state - into a hook that runs in the right order. */
|
|
672
603
|
addStaticResponseHeaders(record) {
|
|
673
604
|
if (this.onResponseHooks.length > 0) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
605
|
+
const observerHeaders = this.onResponseHeaders;
|
|
606
|
+
if (observerHeaders !== undefined) {
|
|
607
|
+
observerHeaders.call(this, (headers) => {
|
|
608
|
+
for (const name of Object.keys(record)) {
|
|
609
|
+
if (!headers.has(name))
|
|
610
|
+
headers.set(name, record[name]);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
return this;
|
|
614
|
+
}
|
|
615
|
+
// Route the hook through the same seam the static tier uses. On a framework response (headers
|
|
616
|
+
// stamped mutable) it applies the defaults in place and returns the SAME object, so the
|
|
617
|
+
// serialized-body marker survives for later `onResponseBody`/`onResponseRaw` observers and the
|
|
618
|
+
// Node writer; reconstructing a `Response` here would strip that marker and reclassify the Node
|
|
619
|
+
// outcome from `json` to a generic `response`. A guarded foreign response takes its clone path.
|
|
620
|
+
const statics = buildStaticResponseHeaders(record);
|
|
621
|
+
this.onResponseHooks.push((response) => applyStaticResponseHeaders(response, statics));
|
|
622
|
+
this.onNodeResponseHooks.push(undefined);
|
|
623
|
+
this.nodeResponseHooksComplete = false;
|
|
624
|
+
return this;
|
|
680
625
|
}
|
|
681
626
|
const merged = this.staticResponseHeaders === undefined
|
|
682
627
|
? record
|
|
683
628
|
: { ...this.staticResponseHeaders.record, ...record };
|
|
684
629
|
this.staticResponseHeaders = buildStaticResponseHeaders(merged);
|
|
685
630
|
const statics = this.staticResponseHeaders;
|
|
686
|
-
this.wrapWebResponse = (response) => applyStaticResponseHeaders(response, statics);
|
|
631
|
+
this.wrapWebResponse = (response) => applyStaticResponseHeaders(webResponseOf(response), statics);
|
|
687
632
|
this.webResponseTimeout = () => applyStaticResponseHeaders(RESPONSE_TIMEOUT(), statics);
|
|
688
633
|
return this;
|
|
689
634
|
}
|
|
@@ -694,58 +639,14 @@ export class Server {
|
|
|
694
639
|
}
|
|
695
640
|
return this.responseBodyTag;
|
|
696
641
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
this.onResponseHooks.push(webResponseRawHook(fn, this.responseBodyOwners));
|
|
706
|
-
this.onNodeResponseHooks.push(() => undefined);
|
|
707
|
-
return this;
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
|
-
* Register a PORTABLE header-only response hook - one implementation, fast on every runtime.
|
|
711
|
-
*
|
|
712
|
-
* The hook receives a mutable case-insensitive header view, a minimal request view, and the
|
|
713
|
-
* status. On the Web serving paths it runs inside the normal `onResponse` walk against the
|
|
714
|
-
* response's own `Headers` (mutating in place - no clone). On Node it self-pairs as a native
|
|
715
|
-
* response hook against the outcome's plain header record, so registering one NEVER forces the
|
|
716
|
-
* Node adapter off its direct socket writer the way a full `onResponse(res: Response)` hook does.
|
|
717
|
-
* Prefer this over `onResponse` whenever the middleware only reads/writes headers.
|
|
718
|
-
*/
|
|
719
|
-
onResponseHeaders(fn) {
|
|
720
|
-
this.assertConfigurable("onResponseHeaders()");
|
|
721
|
-
this.onResponseHooks.push(webResponseHeadersHook(fn));
|
|
722
|
-
this.onNodeResponseHooks.push((response, req) => fn(recordHeadersView(response), req, response.status));
|
|
723
|
-
return this;
|
|
724
|
-
}
|
|
725
|
-
/**
|
|
726
|
-
* Register a PORTABLE post-serialization body hook - the payload tier. The hook receives the
|
|
727
|
-
* FINAL framework-serialized bytes (plus the mutable header view and status) and may return
|
|
728
|
-
* replacement bytes; `undefined` keeps the body. On the Node direct writer the bytes come
|
|
729
|
-
* straight off the outcome record; on the Web serving paths they ride the framework-built
|
|
730
|
-
* Response as a tag, so no body stream is ever drained on any runtime. A handler-returned raw
|
|
731
|
-
* `Response` (proxied fetch, SSE, streamed SSR) is skipped by contract - transforming those is
|
|
732
|
-
* what the full `onResponse` hook is for.
|
|
733
|
-
*/
|
|
734
|
-
onResponseBody(fn) {
|
|
735
|
-
this.assertConfigurable("onResponseBody()");
|
|
736
|
-
this.enableResponseBodyTagging();
|
|
737
|
-
this.onResponseHooks.push(webResponseBodyHook(fn, this.responseBodyOwners));
|
|
738
|
-
this.onNodeResponseHooks.push((response, req) => {
|
|
739
|
-
const body = response.body;
|
|
740
|
-
if (body === null)
|
|
741
|
-
return undefined;
|
|
742
|
-
const out = fn(body, recordHeadersView(response), req, response.status);
|
|
743
|
-
if (out instanceof Promise)
|
|
744
|
-
return out.then((replaced) => applyBodyReplacement(response, replaced));
|
|
745
|
-
applyBodyReplacement(response, out);
|
|
746
|
-
return undefined;
|
|
747
|
-
});
|
|
748
|
-
return this;
|
|
642
|
+
responseObserverMethods() {
|
|
643
|
+
const methods = this;
|
|
644
|
+
if (typeof methods.onResponseHeaders !== "function" ||
|
|
645
|
+
typeof methods.onResponseBody !== "function" ||
|
|
646
|
+
typeof methods.onResponseRaw !== "function") {
|
|
647
|
+
throw new TypeError("response observation requires `.use(responseObserver())` or an observer-enabled middleware");
|
|
648
|
+
}
|
|
649
|
+
return methods;
|
|
749
650
|
}
|
|
750
651
|
/** Observe the terminal response after all transformations. Observers are ordered and fail-open. */
|
|
751
652
|
onResponseFinalized(fn) {
|
|
@@ -791,6 +692,15 @@ export class Server {
|
|
|
791
692
|
}
|
|
792
693
|
this.globalAssurance.push(...evidence.filter((item) => item.scope === "global"));
|
|
793
694
|
this.activeAssurance.push(...evidence.filter((item) => item.scope === "subsequent"));
|
|
695
|
+
const responseObserver = arg[INSTALL_RESPONSE_OBSERVER];
|
|
696
|
+
if (responseObserver !== undefined) {
|
|
697
|
+
if (typeof responseObserver !== "object" ||
|
|
698
|
+
responseObserver === null ||
|
|
699
|
+
typeof responseObserver.install !== "function") {
|
|
700
|
+
throw new TypeError("response observer middleware has an invalid runtime");
|
|
701
|
+
}
|
|
702
|
+
this[INSTALL_RESPONSE_OBSERVER](responseObserver);
|
|
703
|
+
}
|
|
794
704
|
if (arg.onRequest !== undefined) {
|
|
795
705
|
this.assertConfigurable("onRequest()");
|
|
796
706
|
this.onRequestHooks.push(arg.onRequest);
|
|
@@ -824,11 +734,11 @@ export class Server {
|
|
|
824
734
|
if (arg.responseHeaders !== undefined)
|
|
825
735
|
this.responseHeaders(arg.responseHeaders);
|
|
826
736
|
if (arg.onResponseHeaders !== undefined)
|
|
827
|
-
this.onResponseHeaders(arg.onResponseHeaders);
|
|
737
|
+
this.responseObserverMethods().onResponseHeaders(arg.onResponseHeaders);
|
|
828
738
|
if (arg.onResponseBody !== undefined)
|
|
829
|
-
this.onResponseBody(arg.onResponseBody);
|
|
739
|
+
this.responseObserverMethods().onResponseBody(arg.onResponseBody);
|
|
830
740
|
if (arg.onResponseRaw !== undefined)
|
|
831
|
-
this.onResponseRaw(arg.onResponseRaw);
|
|
741
|
+
this.responseObserverMethods().onResponseRaw(arg.onResponseRaw);
|
|
832
742
|
if (arg.onResponseFinalized !== undefined)
|
|
833
743
|
this.onResponseFinalized(arg.onResponseFinalized);
|
|
834
744
|
if (arg.onError !== undefined)
|
|
@@ -935,6 +845,13 @@ export class Server {
|
|
|
935
845
|
// Boot-time guard: the WS runtime is a subpath (`@nifrajs/core/ws`) so no-WebSocket apps don't
|
|
936
846
|
// bundle it. Registration is the loud, early failure point - never the first connection.
|
|
937
847
|
const runtime = requireWsRuntime(this.wsRuntime);
|
|
848
|
+
if (handler.validateSend === true && handler.sendSchema === undefined) {
|
|
849
|
+
throw new RouteConfigError("INVALID_WS_SEND_VALIDATION", `route WS ${path}: validateSend requires sendSchema`);
|
|
850
|
+
}
|
|
851
|
+
// A validated-send route makes broadcast bytes route-dependent (drop-on-invalid per socket), so the
|
|
852
|
+
// whole app forgoes native pub/sub and keeps the JS registry loop that runs each send through the sender.
|
|
853
|
+
if (handler.validateSend === true)
|
|
854
|
+
this.wsHasValidatedSend = true;
|
|
938
855
|
this.topics ??= runtime.createTopics();
|
|
939
856
|
// A `messageSchema` wraps `message` with validation once, here - every adapter then dispatches
|
|
940
857
|
// already-validated, typed messages (Bun/Deno/Node/Workers) with no per-adapter code.
|
|
@@ -950,6 +867,12 @@ export class Server {
|
|
|
950
867
|
* an external fan-out (Redis, a Durable Object) to this. A no-op when nobody is subscribed.
|
|
951
868
|
*/
|
|
952
869
|
publish(topic, data) {
|
|
870
|
+
// Native-pubsub mode (Bun, no validated-send route): one native broadcast to every subscriber, no
|
|
871
|
+
// per-connection JS loop. Bound only after `listen()`; before that (or off Bun) the registry runs.
|
|
872
|
+
if (this.nativePublish !== undefined) {
|
|
873
|
+
this.nativePublish(topic, data);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
953
876
|
// No `app.ws()` yet ⇒ no registry and necessarily no subscribers - a publish is a no-op anyway.
|
|
954
877
|
this.topics?.publish(topic, data);
|
|
955
878
|
}
|
|
@@ -986,197 +909,88 @@ export class Server {
|
|
|
986
909
|
this.catalog.addBatch(staged);
|
|
987
910
|
}
|
|
988
911
|
prepareRoute(method, path, schema, handler) {
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
invalidBodyLimit('bodyLimit must be a non-negative safe integer or "unlimited"');
|
|
1010
|
-
}
|
|
1011
|
-
bodyLimit = schema.bodyLimit;
|
|
1012
|
-
}
|
|
1013
|
-
const capabilities = normalizeRouteCapabilities(schema?.capabilities);
|
|
1014
|
-
const handlerAssurance = assuranceDeclarationsOf(handler);
|
|
1015
|
-
const invalidHandlerScope = handlerAssurance.find((declaration) => declaration.scope !== "plugin");
|
|
1016
|
-
if (invalidHandlerScope !== undefined) {
|
|
1017
|
-
throw new RouteConfigError("INVALID_ASSURANCE", `route handler assurance must use plugin scope (received ${invalidHandlerScope.scope})`);
|
|
1018
|
-
}
|
|
1019
|
-
const authenticated = assuranceEvidenceFor([...this.activeAssurance, ...handlerAssurance, ...this.globalAssurance], method, path).some((evidence) => evidence.id === NIFRA_ASSURANCE_IDS.AUTHENTICATED);
|
|
1020
|
-
const routeDecorations = { ...this.decorations };
|
|
1021
|
-
if (capabilities.length > 0) {
|
|
1022
|
-
routeDecorations[CAPABILITY_GUARD] = createCapabilityGuard(capabilities, method, path, this.onCapabilityUse, this.idempotencyRuntime?.trackEffect, Object.freeze([...this.capabilityInterceptors]), Object.freeze([...this.capabilityObservers]));
|
|
1023
|
-
}
|
|
1024
|
-
const hasDecorations = Reflect.ownKeys(routeDecorations).length > 0;
|
|
1025
|
-
// An idempotency route runs a dedupe lane that must buffer the body and capture the response, so it
|
|
1026
|
-
// never takes the fused/native fast path - force it onto the portable matched lane (which routes
|
|
1027
|
-
// through `fetchMatched`, where the dedupe wrapper lives). Fail closed: a route may not declare
|
|
1028
|
-
// idempotency unless the idempotency runtime is installed, so the safety gate can never be silently
|
|
1029
|
-
// dropped by a missing plugin.
|
|
1030
|
-
if (schema?.idempotency !== undefined && this.idempotencyRuntime === undefined) {
|
|
1031
|
-
throw new RouteConfigError("INVALID_IDEMPOTENCY", "route declares idempotency but the idempotency plugin is not installed; add .use(idempotency())");
|
|
1032
|
-
}
|
|
1033
|
-
const idempotent = this.idempotencyRuntime?.resolve(schema, authenticated, this.maxBodyBytes);
|
|
1034
|
-
// A ledgered route (capabilities declared + `.use(effectLedger())`) needs a per-request
|
|
1035
|
-
// context to carry the ledger and a settle step to seal + sink it, so it too leaves the
|
|
1036
|
-
// fused/contextless fast path. Resolved per route, at registration - like the capability guard.
|
|
1037
|
-
const ledgered = this.effectLedgerRuntime?.resolve(capabilities, method, path);
|
|
1038
|
-
// A checked response schema needs the handler's VALUE before it becomes bytes, and the fused and
|
|
1039
|
-
// native lanes exist precisely to skip that step. So a contracted route leaves them - the same
|
|
1040
|
-
// structural trade an idempotent route makes above. The check itself is not the cost: a compiled
|
|
1041
|
-
// validator measures ~100ns/response, within benchmark noise on any route these lanes already
|
|
1042
|
-
// exclude (middleware, derives, lifecycle hooks).
|
|
1043
|
-
const runtime = this.responseContractRuntime;
|
|
1044
|
-
const contracted = runtime !== undefined && schema?.response !== undefined
|
|
1045
|
-
? { runtime, schema: schema.response }
|
|
1046
|
-
: undefined;
|
|
1047
|
-
const bare = schema?.params === undefined &&
|
|
1048
|
-
schema?.headers === undefined &&
|
|
1049
|
-
schema?.body === undefined &&
|
|
1050
|
-
schema?.query === undefined &&
|
|
1051
|
-
idempotent === undefined &&
|
|
1052
|
-
ledgered === undefined &&
|
|
1053
|
-
contracted === undefined &&
|
|
1054
|
-
this.derives.length === 0 &&
|
|
1055
|
-
this.beforeHandleHooks.length === 0 &&
|
|
1056
|
-
this.afterHandleHooks.length === 0 &&
|
|
1057
|
-
this.onErrorHooks.length === 0;
|
|
1058
|
-
// A route whose ONLY lifecycle step is a query schema can fuse too: the parse + validate +
|
|
1059
|
-
// handler + respond collapse into one closure with no lifecycle promise on the sync path. The
|
|
1060
|
-
// guards mirror the `query` lane below PLUS everything the fused dispatch skips: around hooks,
|
|
1061
|
-
// the idempotency/ledger wrappers, and validation-error recovery (schema or server default) -
|
|
1062
|
-
// the fused invalid path is exactly `validationError(issues)`, so any recovery semantics keep
|
|
1063
|
-
// the generic lane.
|
|
1064
|
-
const fusedQuery = !bare &&
|
|
1065
|
-
contracted === undefined &&
|
|
1066
|
-
schema?.query !== undefined &&
|
|
1067
|
-
schema.body === undefined &&
|
|
1068
|
-
schema.params === undefined &&
|
|
1069
|
-
schema.headers === undefined &&
|
|
1070
|
-
schema.onValidationError === undefined &&
|
|
1071
|
-
this.defaultOnValidationError === undefined &&
|
|
1072
|
-
idempotent === undefined &&
|
|
1073
|
-
ledgered === undefined &&
|
|
1074
|
-
this.derives.length === 0 &&
|
|
1075
|
-
this.beforeHandleHooks.length === 0 &&
|
|
1076
|
-
this.afterHandleHooks.length === 0 &&
|
|
1077
|
-
this.onErrorHooks.length === 0 &&
|
|
1078
|
-
this.aroundHooks.length === 0;
|
|
1079
|
-
const bodyOnly = contracted === undefined &&
|
|
1080
|
-
schema?.body !== undefined &&
|
|
1081
|
-
schema.query === undefined &&
|
|
1082
|
-
schema.params === undefined &&
|
|
1083
|
-
schema.headers === undefined &&
|
|
1084
|
-
this.derives.length === 0 &&
|
|
1085
|
-
this.beforeHandleHooks.length === 0 &&
|
|
1086
|
-
this.afterHandleHooks.length === 0 &&
|
|
1087
|
-
this.onErrorHooks.length === 0;
|
|
1088
|
-
const fusedBody = !bare &&
|
|
1089
|
-
bodyOnly &&
|
|
1090
|
-
schema.onValidationError === undefined &&
|
|
1091
|
-
this.defaultOnValidationError === undefined &&
|
|
1092
|
-
idempotent === undefined &&
|
|
1093
|
-
ledgered === undefined &&
|
|
1094
|
-
this.aroundHooks.length === 0;
|
|
1095
|
-
// Body-only routes still need an async body read, but eligible routes can compile the validation +
|
|
1096
|
-
// handler continuation once. The runner keeps the bounded parser and all error semantics while
|
|
1097
|
-
// avoiding the generic entry/schema/lifecycle dispatch on Bun, Deno, and Node-direct.
|
|
912
|
+
const compiled = compileRouteOptions({
|
|
913
|
+
maxBodyBytes: this.maxBodyBytes,
|
|
914
|
+
activeAssurance: this.activeAssurance,
|
|
915
|
+
globalAssurance: this.globalAssurance,
|
|
916
|
+
decorations: this.decorations,
|
|
917
|
+
onCapabilityUse: this.onCapabilityUse,
|
|
918
|
+
capabilityInterceptors: this.capabilityInterceptors,
|
|
919
|
+
capabilityObservers: this.capabilityObservers,
|
|
920
|
+
effectLedgerRuntime: this.effectLedgerRuntime,
|
|
921
|
+
idempotencyRuntime: this.idempotencyRuntime,
|
|
922
|
+
responseContractRuntime: this.responseContractRuntime,
|
|
923
|
+
derives: this.derives,
|
|
924
|
+
beforeHandleHooks: this.beforeHandleHooks,
|
|
925
|
+
afterHandleHooks: this.afterHandleHooks,
|
|
926
|
+
onErrorHooks: this.onErrorHooks,
|
|
927
|
+
aroundHooks: this.aroundHooks,
|
|
928
|
+
defaultOnValidationError: this.defaultOnValidationError,
|
|
929
|
+
}, method, path, schema, handler);
|
|
930
|
+
const { pattern, bodyLimit, capabilities, routeDecorations, hasDecorations, idempotent, ledgered, responseContract, lanes, routeAssurance, } = compiled;
|
|
931
|
+
const { bare, fusedQuery, fusedBody } = lanes;
|
|
1098
932
|
const fusedBodyRunner = fusedBody
|
|
1099
|
-
? this.buildFusedBodyRunner(handler, schema
|
|
933
|
+
? this.buildFusedBodyRunner(handler, schema?.body, hasDecorations ? routeDecorations : undefined, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1100
934
|
: undefined;
|
|
935
|
+
// Fused lifecycle lanes: derive + before (with or without an after), and body + the same shape.
|
|
936
|
+
// The lane selectors in `selectRouteLanes` are exhaustive about the lifecycleHookLane /
|
|
937
|
+
// body-derive-before-after eligibility, so we only need to check the lane and the hook counts.
|
|
938
|
+
const lifecycleHookLane = lanes.lifecycleHookLane;
|
|
939
|
+
const isFusedLifecycle = lifecycleHookLane !== undefined && this.derives.length === 1;
|
|
940
|
+
const isFusedBodyLifecycle = lanes.fusedLane === "body-derive-before" || lanes.fusedLane === "body-derive-before-after";
|
|
1101
941
|
const fusedWeb = bare && this.aroundHooks.length === 0
|
|
1102
|
-
? this.buildFusedWeb(handler, hasDecorations ? routeDecorations : undefined, isContextlessNoArgArrow(handler), bodyLimit ??
|
|
942
|
+
? this.buildFusedWeb(handler, hasDecorations ? routeDecorations : undefined, isContextlessNoArgArrow(handler), bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1103
943
|
: fusedQuery
|
|
1104
|
-
? this.buildFusedQueryWeb(handler, hasDecorations ? routeDecorations : undefined, schema
|
|
944
|
+
? this.buildFusedQueryWeb(handler, hasDecorations ? routeDecorations : undefined, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1105
945
|
: fusedBody
|
|
1106
946
|
? this.buildFusedBodyWeb(fusedBodyRunner)
|
|
1107
|
-
:
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
// so the request path never re-checks params/body presence. Parameter-schema routes retain the
|
|
1129
|
-
// generic lifecycle runner until their more involved recovery matrix is selected explicitly.
|
|
1130
|
-
const lifecycleLane = lane !== "lifecycle" || schema?.params !== undefined || schema?.headers !== undefined
|
|
1131
|
-
? undefined
|
|
1132
|
-
: schema?.body !== undefined
|
|
1133
|
-
? schema.query !== undefined
|
|
1134
|
-
? "body-query"
|
|
1135
|
-
: "body"
|
|
1136
|
-
: schema?.query !== undefined
|
|
1137
|
-
? "query"
|
|
1138
|
-
: "hooks";
|
|
1139
|
-
// The realistic middleware shape is commonly exactly one synchronous-or-async derive followed
|
|
1140
|
-
// by one before hook. Keep the generic runner for every route that can observe decorations,
|
|
1141
|
-
// after hooks, error hooks, or response contracts; this lane only removes the two per-request
|
|
1142
|
-
// hook-loop dispatches and preserves the same async continuations and error handling.
|
|
1143
|
-
const lifecycleHookLane = lane === "lifecycle" &&
|
|
1144
|
-
contracted === undefined &&
|
|
1145
|
-
!hasDecorations &&
|
|
1146
|
-
this.derives.length === 1 &&
|
|
1147
|
-
this.beforeHandleHooks.length === 1 &&
|
|
1148
|
-
this.afterHandleHooks.length === 0 &&
|
|
1149
|
-
this.onErrorHooks.length === 0
|
|
1150
|
-
? "derive-before"
|
|
1151
|
-
: undefined;
|
|
947
|
+
: isFusedBodyLifecycle
|
|
948
|
+
? this.buildFusedBodyDeriveBeforeAfter(handler, this.derives[0], this.beforeHandleHooks[0], this.afterHandleHooks[0], schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
949
|
+
: isFusedLifecycle && lifecycleHookLane === "derive-before"
|
|
950
|
+
? this.buildFusedDeriveBefore(handler, this.derives[0], this.beforeHandleHooks[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
951
|
+
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
952
|
+
? this.buildFusedDeriveBeforeAfter(handler, this.derives[0], this.beforeHandleHooks[0], this.afterHandleHooks[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
953
|
+
: undefined;
|
|
954
|
+
const program = compileRouteProgram({
|
|
955
|
+
schema,
|
|
956
|
+
handler: handler,
|
|
957
|
+
derives: this.derives,
|
|
958
|
+
beforeHandle: this.beforeHandleHooks,
|
|
959
|
+
afterHandle: this.afterHandleHooks,
|
|
960
|
+
onError: this.onErrorHooks,
|
|
961
|
+
decorations: routeDecorations,
|
|
962
|
+
hasDecorations,
|
|
963
|
+
bodySchema: schema?.body,
|
|
964
|
+
bodyLimit,
|
|
965
|
+
responseContract,
|
|
966
|
+
});
|
|
967
|
+
const contextless = lanes.bare && this.aroundHooks.length === 0 && isContextlessNoArgArrow(handler);
|
|
1152
968
|
const execution = compileRouteExecutionPlan({
|
|
1153
|
-
lane,
|
|
969
|
+
lane: lanes.lane,
|
|
1154
970
|
contextless,
|
|
1155
971
|
hasAround: this.aroundHooks.length > 0,
|
|
1156
972
|
hasLedger: ledgered !== undefined,
|
|
1157
|
-
lifecycleLane,
|
|
1158
973
|
fusedWeb,
|
|
1159
974
|
fusedBody: fusedBodyRunner,
|
|
1160
|
-
fusedLane:
|
|
975
|
+
fusedLane: lanes.fusedLane,
|
|
1161
976
|
});
|
|
1162
977
|
const registeredEntry = {
|
|
1163
|
-
// (context: never) => unknown -> InternalHandler: the framework invokes it
|
|
1164
|
-
// with the concrete RawContext the typed handler expects, so this is sound.
|
|
1165
978
|
handler: handler,
|
|
1166
979
|
schema,
|
|
1167
980
|
bodyLimit,
|
|
1168
981
|
idempotent,
|
|
1169
982
|
ledgered,
|
|
1170
|
-
responseContract
|
|
983
|
+
responseContract,
|
|
1171
984
|
derives: [...this.derives],
|
|
1172
985
|
decorations: routeDecorations,
|
|
1173
986
|
hasDecorations,
|
|
1174
987
|
beforeHandle: [...this.beforeHandleHooks],
|
|
1175
988
|
afterHandle: [...this.afterHandleHooks],
|
|
1176
989
|
onError: [...this.onErrorHooks],
|
|
1177
|
-
lifecycleHookLane,
|
|
990
|
+
lifecycleHookLane: lanes.lifecycleHookLane,
|
|
1178
991
|
around: [...this.aroundHooks],
|
|
1179
992
|
execution,
|
|
993
|
+
program,
|
|
1180
994
|
};
|
|
1181
995
|
const descriptor = {
|
|
1182
996
|
method,
|
|
@@ -1185,53 +999,13 @@ export class Server {
|
|
|
1185
999
|
...(capabilities.length > 0 ? { capabilities } : {}),
|
|
1186
1000
|
...(schema?.family === true ? { family: true } : {}),
|
|
1187
1001
|
};
|
|
1188
|
-
const routeAssurance = [...this.activeAssurance, ...handlerAssurance];
|
|
1189
|
-
if (contracted?.runtime.mode === "enforce" && schema?.response !== undefined) {
|
|
1190
|
-
routeAssurance.push(Object.freeze({
|
|
1191
|
-
id: NIFRA_ASSURANCE_IDS.RESPONSE_CONTRACT,
|
|
1192
|
-
source: "response-contract",
|
|
1193
|
-
scope: "plugin",
|
|
1194
|
-
}));
|
|
1195
|
-
}
|
|
1196
|
-
// Inline `schema.assurance`: the route DECLARES its enforcement evidence adjacent to the handler, so an
|
|
1197
|
-
// in-handler-guarded route satisfies a policy `require:` clause without a `withRouteAssurance` middleware
|
|
1198
|
-
// rewrite. Each id becomes route-scoped `declared` evidence (invalid ids fail closed at registration).
|
|
1199
|
-
for (const id of schema?.assurance ?? []) {
|
|
1200
|
-
if (!validEvidenceId(id)) {
|
|
1201
|
-
throw new Error(`route assurance: invalid evidence id ${JSON.stringify(id)} on ${method} ${path} (use lowercase dot/dash segments)`);
|
|
1202
|
-
}
|
|
1203
|
-
const declared = { id, source: "declared", scope: "plugin" };
|
|
1204
|
-
Object.defineProperty(declared, "provenance", {
|
|
1205
|
-
value: "declared",
|
|
1206
|
-
enumerable: false,
|
|
1207
|
-
configurable: false,
|
|
1208
|
-
writable: false,
|
|
1209
|
-
});
|
|
1210
|
-
routeAssurance.push(Object.freeze(declared));
|
|
1211
|
-
}
|
|
1212
|
-
if (schema?.body !== undefined) {
|
|
1213
|
-
routeAssurance.push(Object.freeze({
|
|
1214
|
-
id: NIFRA_ASSURANCE_IDS.BODY_BOUNDED,
|
|
1215
|
-
source: "route-schema",
|
|
1216
|
-
scope: "plugin",
|
|
1217
|
-
}));
|
|
1218
|
-
}
|
|
1219
|
-
// Declaring `schema.idempotency` is evidence for request replay only. It deliberately never proves
|
|
1220
|
-
// durable command execution; that stronger evidence belongs to a command/outbox adapter.
|
|
1221
|
-
if (schema?.idempotency !== undefined) {
|
|
1222
|
-
routeAssurance.push(Object.freeze({
|
|
1223
|
-
id: NIFRA_ASSURANCE_IDS.IDEMPOTENCY_KEY,
|
|
1224
|
-
source: "route-schema",
|
|
1225
|
-
scope: "plugin",
|
|
1226
|
-
}));
|
|
1227
|
-
}
|
|
1228
1002
|
return {
|
|
1229
1003
|
method,
|
|
1230
1004
|
path,
|
|
1231
1005
|
pattern,
|
|
1232
1006
|
entry: registeredEntry,
|
|
1233
1007
|
descriptor,
|
|
1234
|
-
assurance:
|
|
1008
|
+
assurance: routeAssurance,
|
|
1235
1009
|
};
|
|
1236
1010
|
}
|
|
1237
1011
|
/** The idempotency lane's bridge back into the normal matched lanes, resolved to a concrete Response
|
|
@@ -1244,6 +1018,29 @@ export class Server {
|
|
|
1244
1018
|
this.assertConfigurable("responseContract()");
|
|
1245
1019
|
this.responseContractRuntime = runtime;
|
|
1246
1020
|
}
|
|
1021
|
+
/** @internal Symbol-keyed install seam for the response observer plugin. */
|
|
1022
|
+
[INSTALL_RESPONSE_OBSERVER](runtime) {
|
|
1023
|
+
this.assertConfigurable("responseObserver()");
|
|
1024
|
+
const methods = runtime.install({
|
|
1025
|
+
assertConfigurable: (operation) => this.assertConfigurable(operation),
|
|
1026
|
+
addResponseHook: (web, node) => {
|
|
1027
|
+
this.onResponseHooks.push(web);
|
|
1028
|
+
this.onNodeResponseHooks.push(node);
|
|
1029
|
+
if (node === undefined)
|
|
1030
|
+
this.nodeResponseHooksComplete = false;
|
|
1031
|
+
},
|
|
1032
|
+
enableResponseBodyTagging: () => {
|
|
1033
|
+
if (this.responseBodyTag === undefined) {
|
|
1034
|
+
this.responseBodyTag = Object.freeze({});
|
|
1035
|
+
this.responseBodyOwners.add(this.responseBodyTag);
|
|
1036
|
+
}
|
|
1037
|
+
return this.responseBodyTag;
|
|
1038
|
+
},
|
|
1039
|
+
responseBodyOwners: () => this.responseBodyOwners,
|
|
1040
|
+
});
|
|
1041
|
+
Object.assign(this, methods);
|
|
1042
|
+
return methods;
|
|
1043
|
+
}
|
|
1247
1044
|
/** @internal Symbol-keyed install seam for the `idempotency()` plugin. Off the public typed surface. */
|
|
1248
1045
|
[INSTALL_IDEMPOTENCY](runtime) {
|
|
1249
1046
|
this.assertConfigurable("idempotency()");
|
|
@@ -1373,14 +1170,37 @@ export class Server {
|
|
|
1373
1170
|
return route;
|
|
1374
1171
|
// Rebuild with the SAME builder that produced the closure - rebinding a query-fused route as
|
|
1375
1172
|
// bare would silently drop its validation.
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1173
|
+
const lane = entry.execution.fusedLane;
|
|
1174
|
+
// Type-erase the lane so each `===` branch sees the full union, not the narrowed remainder.
|
|
1175
|
+
const laneName = lane;
|
|
1176
|
+
// Body+lifecycle routes have a fused Web renderer, but no body-only Node renderer: the latter
|
|
1177
|
+
// would bypass derive/before/after because the Node-direct dispatcher prefers `fusedBody` over
|
|
1178
|
+
// the generic execution plan. Only the original body-only lane may populate this slot.
|
|
1179
|
+
const fusedBody = laneName === "body"
|
|
1180
|
+
? this.buildFusedBodyRunner(entry.handler, entry.schema?.body, entry.hasDecorations ? entry.decorations : undefined, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1378
1181
|
: undefined;
|
|
1379
|
-
const
|
|
1182
|
+
const derive = entry.derives[0];
|
|
1183
|
+
const before = entry.beforeHandle[0];
|
|
1184
|
+
const after = entry.afterHandle[0];
|
|
1185
|
+
const fusedWeb = laneName === "body"
|
|
1380
1186
|
? this.buildFusedBodyWeb(fusedBody)
|
|
1381
|
-
:
|
|
1382
|
-
? this.buildFusedQueryWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, entry.schema?.query, entry.bodyLimit ??
|
|
1383
|
-
:
|
|
1187
|
+
: laneName === "query"
|
|
1188
|
+
? this.buildFusedQueryWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1189
|
+
: laneName === "body-derive-before" && derive !== undefined && before !== undefined
|
|
1190
|
+
? this.buildFusedBodyDeriveBeforeAfter(entry.handler, derive, before, undefined, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1191
|
+
: laneName === "body-derive-before-after" &&
|
|
1192
|
+
derive !== undefined &&
|
|
1193
|
+
before !== undefined &&
|
|
1194
|
+
after !== undefined
|
|
1195
|
+
? this.buildFusedBodyDeriveBeforeAfter(entry.handler, derive, before, after, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1196
|
+
: laneName === "derive-before" && derive !== undefined && before !== undefined
|
|
1197
|
+
? this.buildFusedDeriveBefore(entry.handler, derive, before, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1198
|
+
: laneName === "derive-before-after" &&
|
|
1199
|
+
derive !== undefined &&
|
|
1200
|
+
before !== undefined &&
|
|
1201
|
+
after !== undefined
|
|
1202
|
+
? this.buildFusedDeriveBeforeAfter(entry.handler, derive, before, after, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1203
|
+
: this.buildFusedWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, isContextlessNoArgArrow(entry.handler), entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
|
|
1384
1204
|
return {
|
|
1385
1205
|
...route,
|
|
1386
1206
|
entry: {
|
|
@@ -1498,6 +1318,42 @@ export class Server {
|
|
|
1498
1318
|
release();
|
|
1499
1319
|
return outcome;
|
|
1500
1320
|
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Select a mounted handler's native Node lane without materializing a Web Request. This is an
|
|
1323
|
+
* adapter seam, not a second lifecycle: any app-wide behavior that the ordinary mount path would
|
|
1324
|
+
* run makes the capability ineligible and the caller falls back to `resolveNodeSource`. Typed
|
|
1325
|
+
* routes keep precedence exactly as `routeAndRun` does, including the existing mount behavior for
|
|
1326
|
+
* a path whose registered route rejects this method.
|
|
1327
|
+
*/
|
|
1328
|
+
[RESOLVE_NODE_MOUNT](source) {
|
|
1329
|
+
if (this.onRequestHooks.length > 0 ||
|
|
1330
|
+
this.onResponseHooks.length > 0 ||
|
|
1331
|
+
this.onResponseFinalizedHooks.length > 0 ||
|
|
1332
|
+
this.capacityGate !== undefined ||
|
|
1333
|
+
this.staticResponseHeaders !== undefined ||
|
|
1334
|
+
this.requestTimeoutMs !== 0 ||
|
|
1335
|
+
this.acceptInboundDeadlines ||
|
|
1336
|
+
this.clientIpTrust !== undefined) {
|
|
1337
|
+
return undefined;
|
|
1338
|
+
}
|
|
1339
|
+
const parts = source.urlParts ?? urlPartsOf(source.url);
|
|
1340
|
+
const match = this.catalog.find(source.method, parts.pathname);
|
|
1341
|
+
if (match.found)
|
|
1342
|
+
return undefined;
|
|
1343
|
+
for (const mount of this.fetchMounts) {
|
|
1344
|
+
if (!underMountPrefix(parts.pathname, mount.path))
|
|
1345
|
+
continue;
|
|
1346
|
+
const candidate = mount.handler[NODE_NATIVE_MOUNT];
|
|
1347
|
+
return typeof candidate === "function"
|
|
1348
|
+
? {
|
|
1349
|
+
handler: candidate,
|
|
1350
|
+
path: mount.path,
|
|
1351
|
+
stripPrefix: mount.stripPrefix,
|
|
1352
|
+
}
|
|
1353
|
+
: undefined;
|
|
1354
|
+
}
|
|
1355
|
+
return undefined;
|
|
1356
|
+
}
|
|
1501
1357
|
/**
|
|
1502
1358
|
* Like {@link fetch}, but renders a plain-data result **without** building a Web `Response` - the
|
|
1503
1359
|
* `@nifrajs/node` adapter serializes the returned primitives straight to the socket, skipping the undici
|
|
@@ -1983,7 +1839,7 @@ export class Server {
|
|
|
1983
1839
|
/**
|
|
1984
1840
|
* Route → build context → run. Synchronous through to the handler for a **bare** route
|
|
1985
1841
|
* (selected by its compiled plan), so a sync handler produces its result with zero promise allocations;
|
|
1986
|
-
* routes with validation/hooks keep the full
|
|
1842
|
+
* routes with validation/hooks keep the full compiled route program, unchanged.
|
|
1987
1843
|
*/
|
|
1988
1844
|
routeAndRun(source, platform, finalize, wrapResponse, onTimeout, webFast) {
|
|
1989
1845
|
// Routing only needs the pathname. Scan the URL once but keep the two slices in locals instead
|
|
@@ -2037,15 +1893,18 @@ export class Server {
|
|
|
2037
1893
|
: wrapResponse(mounted);
|
|
2038
1894
|
}
|
|
2039
1895
|
if (match.reason === "method-not-allowed") {
|
|
2040
|
-
return wrapResponse(
|
|
1896
|
+
return wrapResponse(
|
|
1897
|
+
// Lowercase on purpose: a plain render's headers go into the node outcome record verbatim,
|
|
1898
|
+
// and every other name in that record is lowercase (the Web lane normalizes either way).
|
|
1899
|
+
plainError(405, "method_not_allowed", { allow: match.allowed.join(", ") }));
|
|
2041
1900
|
}
|
|
2042
|
-
return wrapResponse(
|
|
1901
|
+
return wrapResponse(plainError(404, "not_found"));
|
|
2043
1902
|
}
|
|
2044
1903
|
// Inspect only captured values for escapes. Scanning the full pathname repeated work the router
|
|
2045
1904
|
// already did and made every plain dynamic route pay for unrelated static path bytes.
|
|
2046
1905
|
const params = match.params === EMPTY_PARAMS ? match.params : decodeRouteParams(match.params);
|
|
2047
1906
|
if (params === null) {
|
|
2048
|
-
return wrapResponse(
|
|
1907
|
+
return wrapResponse(plainError(400, "malformed_path"));
|
|
2049
1908
|
}
|
|
2050
1909
|
return this.runMatched(source, platform, match.payload, params, search, finalize, wrapResponse, onTimeout, webFast);
|
|
2051
1910
|
}
|
|
@@ -2080,7 +1939,7 @@ export class Server {
|
|
|
2080
1939
|
// The runtime is always present when a route resolved idempotency (enforced at registration).
|
|
2081
1940
|
if (entry.idempotent !== undefined && this.idempotencyRuntime !== undefined) {
|
|
2082
1941
|
return this.idempotencyRuntime.run(entry.idempotent, requestOf(source), platform, entry, params, search, wrapResponse, {
|
|
2083
|
-
maxBodyBytes: entry.bodyLimit ??
|
|
1942
|
+
maxBodyBytes: entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2084
1943
|
runLanes: (buffered, plat, ent, prm, srch) => this.idempotencyRunLanes(buffered, plat, ent, prm, srch),
|
|
2085
1944
|
});
|
|
2086
1945
|
}
|
|
@@ -2106,7 +1965,7 @@ export class Server {
|
|
|
2106
1965
|
? undefined
|
|
2107
1966
|
: admitDeadline(source.headers, this.deadlineAdmissionOptions);
|
|
2108
1967
|
if (admission !== undefined && !admission.ok) {
|
|
2109
|
-
return wrapResponse(
|
|
1968
|
+
return wrapResponse(plainError(admission.status, admission.reason));
|
|
2110
1969
|
}
|
|
2111
1970
|
const effectiveTimeoutMs = admission?.timeoutMs ?? 0;
|
|
2112
1971
|
// Only allocate a controller for a finite budget; the historical no-timeout path remains
|
|
@@ -2131,7 +1990,7 @@ export class Server {
|
|
|
2131
1990
|
// already complete and can't time out, so it's returned as-is (no 503 race, no promise).
|
|
2132
1991
|
if (controller !== undefined && outcome instanceof Promise) {
|
|
2133
1992
|
const timedOut = admission?.inherited === true
|
|
2134
|
-
? () => wrapResponse(
|
|
1993
|
+
? () => wrapResponse(plainError(504, "deadline_exceeded"))
|
|
2135
1994
|
: onTimeout;
|
|
2136
1995
|
return this.withTimeout(outcome, controller, timedOut, Math.max(0, Math.ceil(budget.remaining())));
|
|
2137
1996
|
}
|
|
@@ -2145,7 +2004,6 @@ export class Server {
|
|
|
2145
2004
|
/** The narrowest bare route: a syntactic `() => ...` handler cannot observe the context argument, so
|
|
2146
2005
|
* successful requests can skip allocating `RequestContext`. Errors still allocate one for logging. */
|
|
2147
2006
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2148
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2149
2007
|
runContextlessBare(entry, source, params, search, signal, budget, platform, finalize, wrapResponse) {
|
|
2150
2008
|
let result;
|
|
2151
2009
|
try {
|
|
@@ -2162,20 +2020,23 @@ export class Server {
|
|
|
2162
2020
|
contextlessBareError(err, source, params, search, signal, budget, platform, wrapResponse) {
|
|
2163
2021
|
if (err instanceof Response)
|
|
2164
2022
|
return wrapResponse(err);
|
|
2023
|
+
// Handed over as the plain data it is: `wrapResponse` renders it on the JSON lane (no `Response`
|
|
2024
|
+
// built on Node). There is no context here by construction, so there is no `c.set` to merge.
|
|
2025
|
+
if (isResponseResult(err))
|
|
2026
|
+
return wrapResponse(err);
|
|
2165
2027
|
const ctx = new RequestContext(source, params, search, signal, budget, platform, this.maxBodyBytes, this.protoPoisoning);
|
|
2166
2028
|
this.logRequestError(err, ctx);
|
|
2167
|
-
return wrapResponse
|
|
2029
|
+
return this.internalErrorResponse(wrapResponse);
|
|
2168
2030
|
}
|
|
2169
2031
|
/**
|
|
2170
2032
|
* The synchronous fast path selected by a route's execution plan: apply static decorations, call the
|
|
2171
2033
|
* handler, render the result - **no `await`** unless the handler itself returns a promise. It mirrors
|
|
2172
|
-
* the
|
|
2034
|
+
* the same error boundary as the compiled route program (which a bare route would otherwise no-op through) and shares
|
|
2173
2035
|
* {@link logRequestError}; a bare route has no `onError` hooks, so error handling is fully synchronous
|
|
2174
2036
|
* (a thrown `Response` is control flow; anything else is a logged flat 500). This is where nifra skips
|
|
2175
2037
|
* the per-request async-frame tax - the same win codegen routers get, but without `eval`.
|
|
2176
2038
|
*/
|
|
2177
2039
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2178
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2179
2040
|
runBare(entry, ctx, finalize, wrapResponse) {
|
|
2180
2041
|
let result;
|
|
2181
2042
|
try {
|
|
@@ -2184,15 +2045,15 @@ export class Server {
|
|
|
2184
2045
|
result = entry.handler(ctx);
|
|
2185
2046
|
}
|
|
2186
2047
|
catch (err) {
|
|
2187
|
-
return this.bareError(err, ctx, wrapResponse);
|
|
2048
|
+
return this.bareError(err, ctx, finalize, wrapResponse);
|
|
2188
2049
|
}
|
|
2189
2050
|
if (result instanceof Promise) {
|
|
2190
2051
|
// Async handler on an otherwise-bare route: finish on a microtask, with the same error handling.
|
|
2191
|
-
return result.then((value) => finalize(value, responseSet(ctx)), (err) => this.bareError(err, ctx, wrapResponse));
|
|
2052
|
+
return result.then((value) => finalize(value, responseSet(ctx)), (err) => this.bareError(err, ctx, finalize, wrapResponse));
|
|
2192
2053
|
}
|
|
2193
2054
|
return finalize(result, responseSet(ctx));
|
|
2194
2055
|
}
|
|
2195
|
-
/** Bare-route error rendering - identical to
|
|
2056
|
+
/** Bare-route error rendering - identical to the compiled route program's catch minus the (absent) onError
|
|
2196
2057
|
* loop: a thrown `Response` is returned as deliberate control flow; anything else is logged + 500. */
|
|
2197
2058
|
/**
|
|
2198
2059
|
* Build a route's fused Web renderer. Composition happens once at
|
|
@@ -2202,14 +2063,10 @@ export class Server {
|
|
|
2202
2063
|
* 500s), same respond semantics (the lifecycle parity suite pins it).
|
|
2203
2064
|
*/
|
|
2204
2065
|
buildFusedWeb(handler, decorations, contextless, maxBodyBytes) {
|
|
2205
|
-
//
|
|
2206
|
-
//
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
return this.wrapWebResponse(err);
|
|
2210
|
-
this.logRequestError(err, ctx);
|
|
2211
|
-
return this.wrapWebResponse(jsonError(500, "internal_error"));
|
|
2212
|
-
};
|
|
2066
|
+
// Same thrown-value contract as the generic lane (`bare-error-lane.ts` via `bareError`); the fused
|
|
2067
|
+
// lane only injects its renderer, so a thrown `status(...)` and the flat 500 fold in static headers
|
|
2068
|
+
// through `wrapWebResponse`. One source of truth for the error surface, no per-lane copy to drift.
|
|
2069
|
+
const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
|
|
2213
2070
|
if (contextless && decorations === undefined) {
|
|
2214
2071
|
// `() => ...` can't observe the context - skip allocating one entirely (errors still build
|
|
2215
2072
|
// one for the structured log, exactly like runContextlessBare).
|
|
@@ -2255,7 +2112,7 @@ export class Server {
|
|
|
2255
2112
|
* bounded parser remains shared with the generic lane; only the route-invariant entry lookups and
|
|
2256
2113
|
* lifecycle dispatch disappear from the common synchronous-validator/synchronous-handler case. */
|
|
2257
2114
|
buildFusedBodyRunner(handler, bodySchema, decorations, maxBodyBytes) {
|
|
2258
|
-
const logError = (err, ctx, wrapResponse) => this.bareError(err, ctx, wrapResponse);
|
|
2115
|
+
const logError = (err, ctx, finalize, wrapResponse) => this.bareError(err, ctx, finalize, wrapResponse);
|
|
2259
2116
|
const runHandler = (ctx, finalize, wrapResponse) => {
|
|
2260
2117
|
if (decorations !== undefined)
|
|
2261
2118
|
Object.assign(ctx, decorations);
|
|
@@ -2264,7 +2121,7 @@ export class Server {
|
|
|
2264
2121
|
output = handler(ctx);
|
|
2265
2122
|
}
|
|
2266
2123
|
catch (err) {
|
|
2267
|
-
return logError(err, ctx, wrapResponse);
|
|
2124
|
+
return logError(err, ctx, finalize, wrapResponse);
|
|
2268
2125
|
}
|
|
2269
2126
|
if (output instanceof Promise) {
|
|
2270
2127
|
return output.then((result) => {
|
|
@@ -2272,20 +2129,20 @@ export class Server {
|
|
|
2272
2129
|
return finalize(result, responseSet(ctx), ctx);
|
|
2273
2130
|
}
|
|
2274
2131
|
catch (err) {
|
|
2275
|
-
return logError(err, ctx, wrapResponse);
|
|
2132
|
+
return logError(err, ctx, finalize, wrapResponse);
|
|
2276
2133
|
}
|
|
2277
|
-
}, (err) => logError(err, ctx, wrapResponse));
|
|
2134
|
+
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
2278
2135
|
}
|
|
2279
2136
|
try {
|
|
2280
2137
|
return finalize(output, responseSet(ctx), ctx);
|
|
2281
2138
|
}
|
|
2282
2139
|
catch (err) {
|
|
2283
|
-
return logError(err, ctx, wrapResponse);
|
|
2140
|
+
return logError(err, ctx, finalize, wrapResponse);
|
|
2284
2141
|
}
|
|
2285
2142
|
};
|
|
2286
2143
|
const runValidated = (result, ctx, finalize, wrapResponse) => {
|
|
2287
2144
|
if (result.issues !== undefined)
|
|
2288
|
-
return wrapResponse(
|
|
2145
|
+
return wrapResponse(plainValidationError(result.issues));
|
|
2289
2146
|
ctx.body = result.value;
|
|
2290
2147
|
return runHandler(ctx, finalize, wrapResponse);
|
|
2291
2148
|
};
|
|
@@ -2295,7 +2152,7 @@ export class Server {
|
|
|
2295
2152
|
validation = bodySchema["~standard"].validate(parsed);
|
|
2296
2153
|
}
|
|
2297
2154
|
catch (err) {
|
|
2298
|
-
return logError(err, ctx, wrapResponse);
|
|
2155
|
+
return logError(err, ctx, finalize, wrapResponse);
|
|
2299
2156
|
}
|
|
2300
2157
|
if (validation instanceof Promise) {
|
|
2301
2158
|
return validation.then((settled) => {
|
|
@@ -2303,15 +2160,15 @@ export class Server {
|
|
|
2303
2160
|
return runValidated(settled, ctx, finalize, wrapResponse);
|
|
2304
2161
|
}
|
|
2305
2162
|
catch (err) {
|
|
2306
|
-
return logError(err, ctx, wrapResponse);
|
|
2163
|
+
return logError(err, ctx, finalize, wrapResponse);
|
|
2307
2164
|
}
|
|
2308
|
-
}, (err) => logError(err, ctx, wrapResponse));
|
|
2165
|
+
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
2309
2166
|
}
|
|
2310
2167
|
try {
|
|
2311
2168
|
return runValidated(validation, ctx, finalize, wrapResponse);
|
|
2312
2169
|
}
|
|
2313
2170
|
catch (err) {
|
|
2314
|
-
return logError(err, ctx, wrapResponse);
|
|
2171
|
+
return logError(err, ctx, finalize, wrapResponse);
|
|
2315
2172
|
}
|
|
2316
2173
|
};
|
|
2317
2174
|
return (source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse) => {
|
|
@@ -2319,7 +2176,7 @@ export class Server {
|
|
|
2319
2176
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
2320
2177
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
2321
2178
|
const finish = (value) => runParsed(value, ctx, finalize, wrapResponse);
|
|
2322
|
-
return
|
|
2179
|
+
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, finish, wrapResponse, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
2323
2180
|
};
|
|
2324
2181
|
}
|
|
2325
2182
|
/** Web adapter wrapper for the shared body runner. Node passes its native finalizer directly through
|
|
@@ -2335,14 +2192,10 @@ export class Server {
|
|
|
2335
2192
|
* `Response` passes through, anything else logs and returns a flat 500, and an async validator
|
|
2336
2193
|
* falls to a then-chain with the same steps. */
|
|
2337
2194
|
buildFusedQueryWeb(handler, decorations, querySchema, maxBodyBytes) {
|
|
2338
|
-
//
|
|
2339
|
-
//
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
return this.wrapWebResponse(err);
|
|
2343
|
-
this.logRequestError(err, ctx);
|
|
2344
|
-
return this.wrapWebResponse(jsonError(500, "internal_error"));
|
|
2345
|
-
};
|
|
2195
|
+
// Same thrown-value contract as the generic lane (`bare-error-lane.ts` via `bareError`); the fused
|
|
2196
|
+
// lane only injects its renderer, so a thrown `status(...)` and the flat 500 fold in static headers
|
|
2197
|
+
// through `wrapWebResponse`. One source of truth for the error surface, no per-lane copy to drift.
|
|
2198
|
+
const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
|
|
2346
2199
|
const runHandler = (ctx, value) => {
|
|
2347
2200
|
ctx.query = value;
|
|
2348
2201
|
let result;
|
|
@@ -2372,22 +2225,346 @@ export class Server {
|
|
|
2372
2225
|
}
|
|
2373
2226
|
if (validation instanceof Promise) {
|
|
2374
2227
|
return validation.then((settled) => settled.issues !== undefined
|
|
2375
|
-
? this.wrapWebResponse(
|
|
2228
|
+
? this.wrapWebResponse(plainValidationError(settled.issues))
|
|
2376
2229
|
: runHandler(ctx, settled.value), (err) => logError(err, ctx));
|
|
2377
2230
|
}
|
|
2378
2231
|
if (validation.issues !== undefined)
|
|
2379
|
-
return this.wrapWebResponse(
|
|
2232
|
+
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
2380
2233
|
return runHandler(ctx, validation.value);
|
|
2381
2234
|
};
|
|
2382
2235
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2236
|
+
/**
|
|
2237
|
+
* Compile a route's `derive + before` lifecycle to a single closure: the per-request walk through
|
|
2238
|
+
* `runSync` → `runHooksSync` is a stack of 3 frames and 2 `Object.assign`s for what is by far the
|
|
2239
|
+
* most common middleware shape (auth/trace/id derivation, an auth/policy `beforeHandle`). Folding
|
|
2240
|
+
* the same steps into one closure hits the same monomorphic inline-cache site as `buildFusedWeb`,
|
|
2241
|
+
* and `Object.assign(ctx, deriveResult)` becomes one extra property write on the context's already
|
|
2242
|
+
* hot hidden class. Behavior is byte-identical to the compiled route program: the same thrown-value
|
|
2243
|
+
* contract (`Response` is control flow, anything else logs + 500), the same `c.set` semantics, the
|
|
2244
|
+
* same `fusedRespond` for the rendered body. The `LifecycleExecutionLane` guarantee
|
|
2245
|
+
* (`schema.params === undefined && schema.headers === undefined && derives === 1 &&
|
|
2246
|
+
* beforeHandle === 1 && afterHandle === 0 && onError === 0 && !hasResponseContract &&
|
|
2247
|
+
* !hasDecorations`) is enforced by `selectRouteLanes`; this lane is selected ONLY when all of those
|
|
2248
|
+
* hold. A `query` schema (the route declares `{ query }` alongside the hooks) is validated FIRST,
|
|
2249
|
+
* mirroring the compiled route program's stage order; the fused builder runs validate → derive →
|
|
2250
|
+
* before → handler in one frame.
|
|
2251
|
+
*/
|
|
2252
|
+
buildFusedDeriveBefore(handler, derive, before, querySchema, maxBodyBytes) {
|
|
2253
|
+
const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
|
|
2254
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
2255
|
+
const ctx = nativeContext
|
|
2256
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
2257
|
+
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
2258
|
+
const runHandler = () => {
|
|
2259
|
+
let result;
|
|
2260
|
+
try {
|
|
2261
|
+
result = handler(ctx);
|
|
2262
|
+
}
|
|
2263
|
+
catch (err) {
|
|
2264
|
+
return logError(err, ctx);
|
|
2265
|
+
}
|
|
2266
|
+
if (result instanceof Promise) {
|
|
2267
|
+
return result.then((value) => fusedRespond(value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
|
|
2268
|
+
}
|
|
2269
|
+
return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
|
|
2270
|
+
};
|
|
2271
|
+
const runBefore = () => {
|
|
2272
|
+
let early;
|
|
2273
|
+
try {
|
|
2274
|
+
early = before(ctx);
|
|
2275
|
+
}
|
|
2276
|
+
catch (err) {
|
|
2277
|
+
return logError(err, ctx);
|
|
2278
|
+
}
|
|
2279
|
+
if (early instanceof Promise) {
|
|
2280
|
+
return early.then((settled) => {
|
|
2281
|
+
if (settled === undefined)
|
|
2282
|
+
return runHandler();
|
|
2283
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2284
|
+
}, (err) => logError(err, ctx));
|
|
2285
|
+
}
|
|
2286
|
+
if (early === undefined)
|
|
2287
|
+
return runHandler();
|
|
2288
|
+
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2289
|
+
};
|
|
2290
|
+
const runDerive = () => {
|
|
2291
|
+
let derived;
|
|
2292
|
+
try {
|
|
2293
|
+
derived = derive(ctx);
|
|
2294
|
+
}
|
|
2295
|
+
catch (err) {
|
|
2296
|
+
return logError(err, ctx);
|
|
2297
|
+
}
|
|
2298
|
+
if (derived instanceof Promise) {
|
|
2299
|
+
return derived.then((settled) => {
|
|
2300
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
2301
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2302
|
+
}
|
|
2303
|
+
Object.assign(ctx, settled);
|
|
2304
|
+
return runBefore();
|
|
2305
|
+
}, (err) => logError(err, ctx));
|
|
2306
|
+
}
|
|
2307
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
2308
|
+
return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2309
|
+
}
|
|
2310
|
+
Object.assign(ctx, derived);
|
|
2311
|
+
return runBefore();
|
|
2312
|
+
};
|
|
2313
|
+
if (querySchema === undefined)
|
|
2314
|
+
return runDerive();
|
|
2315
|
+
let validation;
|
|
2316
|
+
try {
|
|
2317
|
+
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
|
|
2318
|
+
}
|
|
2319
|
+
catch (err) {
|
|
2320
|
+
return logError(err, ctx);
|
|
2321
|
+
}
|
|
2322
|
+
if (validation instanceof Promise) {
|
|
2323
|
+
return validation.then((settled) => {
|
|
2324
|
+
if (settled.issues !== undefined)
|
|
2325
|
+
return this.wrapWebResponse(plainValidationError(settled.issues));
|
|
2326
|
+
ctx.query = settled.value;
|
|
2327
|
+
return runDerive();
|
|
2328
|
+
}, (err) => logError(err, ctx));
|
|
2329
|
+
}
|
|
2330
|
+
if (validation.issues !== undefined)
|
|
2331
|
+
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
2332
|
+
ctx.query = validation.value;
|
|
2333
|
+
return runDerive();
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Same shape as `buildFusedDeriveBefore`, plus one `afterHandle` step. The closure runs (validate) →
|
|
2338
|
+
* derive → before → handler → after in one frame, the shape the realistic middleware route lands on
|
|
2339
|
+
* when it also wants a response transform (e.g. attaching a request id to a successful payload's
|
|
2340
|
+
* headers). `LifecycleExecutionLane` and `selectRouteLanes` guarantee `afterHandle === 1`, the
|
|
2341
|
+
* throw contract is the same as the generic program, and the `after` step is the final transform +
|
|
2342
|
+
* respond the generic program already does.
|
|
2343
|
+
*/
|
|
2344
|
+
buildFusedDeriveBeforeAfter(handler, derive, before, after, querySchema, maxBodyBytes) {
|
|
2345
|
+
const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
|
|
2346
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
2347
|
+
const ctx = nativeContext
|
|
2348
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
2349
|
+
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
2350
|
+
const runAfter = (result) => {
|
|
2351
|
+
let transformed;
|
|
2352
|
+
try {
|
|
2353
|
+
transformed = after(result, ctx);
|
|
2354
|
+
}
|
|
2355
|
+
catch (err) {
|
|
2356
|
+
return logError(err, ctx);
|
|
2357
|
+
}
|
|
2358
|
+
if (transformed instanceof Promise) {
|
|
2359
|
+
return transformed.then((value) => fusedRespond(value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
|
|
2360
|
+
}
|
|
2361
|
+
return fusedRespond(transformed, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
|
|
2362
|
+
};
|
|
2363
|
+
const runHandler = () => {
|
|
2364
|
+
let result;
|
|
2365
|
+
try {
|
|
2366
|
+
result = handler(ctx);
|
|
2367
|
+
}
|
|
2368
|
+
catch (err) {
|
|
2369
|
+
return logError(err, ctx);
|
|
2370
|
+
}
|
|
2371
|
+
if (result instanceof Promise) {
|
|
2372
|
+
return result.then((value) => runAfter(value), (err) => logError(err, ctx));
|
|
2373
|
+
}
|
|
2374
|
+
return runAfter(result);
|
|
2375
|
+
};
|
|
2376
|
+
const runBefore = () => {
|
|
2377
|
+
let early;
|
|
2378
|
+
try {
|
|
2379
|
+
early = before(ctx);
|
|
2380
|
+
}
|
|
2381
|
+
catch (err) {
|
|
2382
|
+
return logError(err, ctx);
|
|
2383
|
+
}
|
|
2384
|
+
if (early instanceof Promise) {
|
|
2385
|
+
return early.then((settled) => {
|
|
2386
|
+
if (settled === undefined)
|
|
2387
|
+
return runHandler();
|
|
2388
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2389
|
+
}, (err) => logError(err, ctx));
|
|
2390
|
+
}
|
|
2391
|
+
if (early === undefined)
|
|
2392
|
+
return runHandler();
|
|
2393
|
+
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2394
|
+
};
|
|
2395
|
+
const runDerive = () => {
|
|
2396
|
+
let derived;
|
|
2397
|
+
try {
|
|
2398
|
+
derived = derive(ctx);
|
|
2399
|
+
}
|
|
2400
|
+
catch (err) {
|
|
2401
|
+
return logError(err, ctx);
|
|
2402
|
+
}
|
|
2403
|
+
if (derived instanceof Promise) {
|
|
2404
|
+
return derived.then((settled) => {
|
|
2405
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
2406
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2407
|
+
}
|
|
2408
|
+
Object.assign(ctx, settled);
|
|
2409
|
+
return runBefore();
|
|
2410
|
+
}, (err) => logError(err, ctx));
|
|
2411
|
+
}
|
|
2412
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
2413
|
+
return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2414
|
+
}
|
|
2415
|
+
Object.assign(ctx, derived);
|
|
2416
|
+
return runBefore();
|
|
2417
|
+
};
|
|
2418
|
+
if (querySchema === undefined)
|
|
2419
|
+
return runDerive();
|
|
2420
|
+
let validation;
|
|
2421
|
+
try {
|
|
2422
|
+
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
|
|
2423
|
+
}
|
|
2424
|
+
catch (err) {
|
|
2425
|
+
return logError(err, ctx);
|
|
2426
|
+
}
|
|
2427
|
+
if (validation instanceof Promise) {
|
|
2428
|
+
return validation.then((settled) => {
|
|
2429
|
+
if (settled.issues !== undefined)
|
|
2430
|
+
return this.wrapWebResponse(plainValidationError(settled.issues));
|
|
2431
|
+
ctx.query = settled.value;
|
|
2432
|
+
return runDerive();
|
|
2433
|
+
}, (err) => logError(err, ctx));
|
|
2434
|
+
}
|
|
2435
|
+
if (validation.issues !== undefined)
|
|
2436
|
+
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
2437
|
+
ctx.query = validation.value;
|
|
2438
|
+
return runDerive();
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Body + lifecycle hooks fused: parse → validate → derive → before → handler → (after) in one
|
|
2443
|
+
* closure, the same win `buildFusedBodyRunner` gives for the no-hooks body shape. The shape is
|
|
2444
|
+
* `lifecycleHookLane` PLUS a body schema with no other validation conflicts; the lane is selected
|
|
2445
|
+
* ONLY when `schema.body !== undefined && schema.query/params/headers === undefined &&
|
|
2446
|
+
* schema.onValidationError === undefined && !defaultOnValidationError && !hasIdempotency &&
|
|
2447
|
+
* !hasLedger && derives === 1 && beforeHandle === 1 && afterHandle <= 1 && onError === 0 &&
|
|
2448
|
+
* !hasResponseContract && !hasDecorations && around === 0`. The same single `querySchema === undefined`
|
|
2449
|
+
* trick used in the no-body lane lets the build path stay simple: a query + body route falls back
|
|
2450
|
+
* to the generic program (this is rare in practice - validated query + validated body is usually a
|
|
2451
|
+
* "search results" route with a middleware chain on top, and the generic lane is fine there).
|
|
2452
|
+
*/
|
|
2453
|
+
buildFusedBodyDeriveBeforeAfter(handler, derive, before, after, bodySchema, maxBodyBytes) {
|
|
2454
|
+
const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
|
|
2455
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
2456
|
+
const ctx = nativeContext
|
|
2457
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
2458
|
+
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
2459
|
+
const runHandlerValidated = () => {
|
|
2460
|
+
const runAfter = (result) => {
|
|
2461
|
+
if (after === undefined) {
|
|
2462
|
+
return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
|
|
2463
|
+
}
|
|
2464
|
+
let transformed;
|
|
2465
|
+
try {
|
|
2466
|
+
transformed = after(result, ctx);
|
|
2467
|
+
}
|
|
2468
|
+
catch (err) {
|
|
2469
|
+
return logError(err, ctx);
|
|
2470
|
+
}
|
|
2471
|
+
if (transformed instanceof Promise) {
|
|
2472
|
+
return transformed.then((value) => fusedRespond(value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
|
|
2473
|
+
}
|
|
2474
|
+
return fusedRespond(transformed, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
|
|
2475
|
+
};
|
|
2476
|
+
const runHandler = () => {
|
|
2477
|
+
let result;
|
|
2478
|
+
try {
|
|
2479
|
+
result = handler(ctx);
|
|
2480
|
+
}
|
|
2481
|
+
catch (err) {
|
|
2482
|
+
return logError(err, ctx);
|
|
2483
|
+
}
|
|
2484
|
+
if (result instanceof Promise) {
|
|
2485
|
+
return result.then((value) => runAfter(value), (err) => logError(err, ctx));
|
|
2486
|
+
}
|
|
2487
|
+
return runAfter(result);
|
|
2488
|
+
};
|
|
2489
|
+
const runBefore = () => {
|
|
2490
|
+
let early;
|
|
2491
|
+
try {
|
|
2492
|
+
early = before(ctx);
|
|
2493
|
+
}
|
|
2494
|
+
catch (err) {
|
|
2495
|
+
return logError(err, ctx);
|
|
2496
|
+
}
|
|
2497
|
+
if (early instanceof Promise) {
|
|
2498
|
+
return early.then((settled) => {
|
|
2499
|
+
if (settled === undefined)
|
|
2500
|
+
return runHandler();
|
|
2501
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2502
|
+
}, (err) => logError(err, ctx));
|
|
2503
|
+
}
|
|
2504
|
+
if (early === undefined)
|
|
2505
|
+
return runHandler();
|
|
2506
|
+
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2507
|
+
};
|
|
2508
|
+
let derived;
|
|
2509
|
+
try {
|
|
2510
|
+
derived = derive(ctx);
|
|
2511
|
+
}
|
|
2512
|
+
catch (err) {
|
|
2513
|
+
return logError(err, ctx);
|
|
2514
|
+
}
|
|
2515
|
+
if (derived instanceof Promise) {
|
|
2516
|
+
return derived.then((settled) => {
|
|
2517
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
2518
|
+
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2519
|
+
}
|
|
2520
|
+
Object.assign(ctx, settled);
|
|
2521
|
+
return runBefore();
|
|
2522
|
+
}, (err) => logError(err, ctx));
|
|
2523
|
+
}
|
|
2524
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
2525
|
+
return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2526
|
+
}
|
|
2527
|
+
Object.assign(ctx, derived);
|
|
2528
|
+
return runBefore();
|
|
2529
|
+
};
|
|
2530
|
+
const onParsed = (parsed) => {
|
|
2531
|
+
let validation;
|
|
2532
|
+
try {
|
|
2533
|
+
validation = bodySchema["~standard"].validate(parsed);
|
|
2534
|
+
}
|
|
2535
|
+
catch (err) {
|
|
2536
|
+
return logError(err, ctx);
|
|
2537
|
+
}
|
|
2538
|
+
if (validation instanceof Promise) {
|
|
2539
|
+
return validation.then((settled) => {
|
|
2540
|
+
if (settled.issues !== undefined)
|
|
2541
|
+
return this.wrapWebResponse(plainValidationError(settled.issues));
|
|
2542
|
+
ctx.body = settled.value;
|
|
2543
|
+
return runHandlerValidated();
|
|
2544
|
+
}, (err) => logError(err, ctx));
|
|
2545
|
+
}
|
|
2546
|
+
if (validation.issues !== undefined)
|
|
2547
|
+
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
2548
|
+
ctx.body = validation.value;
|
|
2549
|
+
return runHandlerValidated();
|
|
2550
|
+
};
|
|
2551
|
+
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, onParsed, (response) => this.wrapWebResponse(response), (err) => Promise.resolve(logError(err, ctx)));
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
bareError(err, ctx, finalize, wrapResponse) {
|
|
2555
|
+
// The thrown-value contract lives in `bare-error-lane.ts`, one source of truth for both this
|
|
2556
|
+
// lane and the fused body runner. `responseSet` and the logger are injected because they reach
|
|
2557
|
+
// into class state; the closure is allocated only on the throw path, never on the hot lane.
|
|
2558
|
+
return renderBareError(err, ctx, finalize, wrapResponse, responseSet, (e, c) => this.logRequestError(e, c), () => this.internalErrorResponse(wrapResponse));
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* Keep the ordinary thrown-error response independent from the caught value. Deliberate Response
|
|
2562
|
+
* control flow is handled by `renderBareError`; every other throw gets this fixed public envelope.
|
|
2563
|
+
*/
|
|
2564
|
+
internalErrorResponse(wrapResponse) {
|
|
2565
|
+
return wrapResponse(plainError(500, "internal_error"));
|
|
2388
2566
|
}
|
|
2389
2567
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2390
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2391
2568
|
runWithAround(entry, ctx, run, finalize, wrapResponse) {
|
|
2392
2569
|
let outcome;
|
|
2393
2570
|
try {
|
|
@@ -2400,6 +2577,45 @@ export class Server {
|
|
|
2400
2577
|
? outcome.catch((err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse))
|
|
2401
2578
|
: outcome;
|
|
2402
2579
|
}
|
|
2580
|
+
/** Execute the registration-compiled general stage program. */
|
|
2581
|
+
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2582
|
+
runProgram(entry, source, ctx, finalize, wrapResponse) {
|
|
2583
|
+
return executeRouteProgram(this, entry, entry.program, source, ctx, finalize, wrapResponse);
|
|
2584
|
+
}
|
|
2585
|
+
/** Bind a compiled validation stage to its stable schema and the existing recovery contract. */
|
|
2586
|
+
// @ts-expect-error TS6133 -- invoked structurally by the general route program
|
|
2587
|
+
validateProgramStage(entry, stage, source, ctx) {
|
|
2588
|
+
const input = stage.kind === "headers"
|
|
2589
|
+
? headerObjectOf(source.headers)
|
|
2590
|
+
: stage.kind === "params"
|
|
2591
|
+
? ctx.params
|
|
2592
|
+
: stage.kind === "query"
|
|
2593
|
+
? queryObjectOf(ctx[CONTEXT_SEARCH])
|
|
2594
|
+
: undefined;
|
|
2595
|
+
if (stage.kind === "body")
|
|
2596
|
+
return this.readProgramBody(entry, source, ctx);
|
|
2597
|
+
const validation = stage.schema["~standard"].validate(input);
|
|
2598
|
+
return validation instanceof Promise
|
|
2599
|
+
? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, stage.kind))
|
|
2600
|
+
: this.applyLifecycleValidation(entry, validation, ctx, stage.kind);
|
|
2601
|
+
}
|
|
2602
|
+
/** Preserve the existing bounded body reader and its fail-closed framing/prototype checks. */
|
|
2603
|
+
readProgramBody(entry, source, ctx) {
|
|
2604
|
+
const bodySchema = entry.program.bodySchema;
|
|
2605
|
+
if (bodySchema === undefined)
|
|
2606
|
+
return Promise.resolve(plainError(415, "unsupported_media_type"));
|
|
2607
|
+
return this.readAndValidateBody(source, entry, ctx, bodySchema, entry.program.bodyLimit);
|
|
2608
|
+
}
|
|
2609
|
+
/** Keep thrown Response/status and redacted 500 semantics in one existing error boundary. */
|
|
2610
|
+
// @ts-expect-error TS6133 -- invoked structurally by the general route program
|
|
2611
|
+
handleProgramError(entry, error, ctx, finalize, wrapResponse) {
|
|
2612
|
+
return this.handleLifecycleError(entry, error, ctx, finalize, wrapResponse);
|
|
2613
|
+
}
|
|
2614
|
+
/** Finish a successful program result through response-contract enforcement and finalization. */
|
|
2615
|
+
// @ts-expect-error TS6133 -- invoked structurally by the general route program
|
|
2616
|
+
finishProgramResult(entry, ctx, result, finalize, wrapResponse) {
|
|
2617
|
+
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result);
|
|
2618
|
+
}
|
|
2403
2619
|
runAround(hooks, ctx, run) {
|
|
2404
2620
|
const dispatch = (index) => {
|
|
2405
2621
|
if (index >= hooks.length)
|
|
@@ -2416,29 +2632,8 @@ export class Server {
|
|
|
2416
2632
|
return dispatch(0);
|
|
2417
2633
|
}
|
|
2418
2634
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2419
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2420
2635
|
runBodyOnly(entry, source, ctx, finalize, wrapResponse) {
|
|
2421
|
-
return
|
|
2422
|
-
}
|
|
2423
|
-
/** Shared bounded body framing/parser. Both body lanes use the same trust-boundary checks; only
|
|
2424
|
-
* the validation + handler continuation differs. */
|
|
2425
|
-
readBodyInput(source, maxBodyBytes, onParsed, wrapResponse, onError) {
|
|
2426
|
-
const contentType = headerOf(source, "content-type") ?? "";
|
|
2427
|
-
if (contentType !== "application/json" && !contentType.includes("application/json")) {
|
|
2428
|
-
if (isUrlEncodedForm(contentType)) {
|
|
2429
|
-
return readBoundedForm(source, maxBodyBytes).then((form) => (form instanceof Response ? wrapResponse(form) : onParsed(form)), onError);
|
|
2430
|
-
}
|
|
2431
|
-
return Promise.resolve(wrapResponse(jsonError(415, "unsupported_media_type")));
|
|
2432
|
-
}
|
|
2433
|
-
// All JSON bodies go through readBoundedJson - the single enforcement point for the framed
|
|
2434
|
-
// fast path (post-read byte re-check), the streaming cap, and the prototype-poisoning guard.
|
|
2435
|
-
// A separate inlined native-json() fast path here would fork the trust boundary in two.
|
|
2436
|
-
try {
|
|
2437
|
-
return this.readBoundedJson(source, maxBodyBytes, (parsed) => (parsed instanceof Response ? wrapResponse(parsed) : onParsed(parsed)), onError);
|
|
2438
|
-
}
|
|
2439
|
-
catch (err) {
|
|
2440
|
-
return Promise.resolve(onError(err));
|
|
2441
|
-
}
|
|
2636
|
+
return readBodyFramed(source, entry.bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, (parsed) => this.finishBodyOnly(entry, parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2442
2637
|
}
|
|
2443
2638
|
/** Validate + run the handler for the bodyOnly path - shared by the inline fast path and the
|
|
2444
2639
|
* streaming fallback. A method (not per-request closures) so the hot path allocates nothing
|
|
@@ -2493,13 +2688,13 @@ export class Server {
|
|
|
2493
2688
|
if (validation instanceof Promise) {
|
|
2494
2689
|
return validation.then((settled) => {
|
|
2495
2690
|
if (settled.issues !== undefined)
|
|
2496
|
-
return wrapResponse(
|
|
2691
|
+
return wrapResponse(plainValidationError(settled.issues));
|
|
2497
2692
|
ctx.body = settled.value;
|
|
2498
2693
|
return this.executeHandler(entry, ctx, finalize);
|
|
2499
2694
|
});
|
|
2500
2695
|
}
|
|
2501
2696
|
if (validation.issues !== undefined)
|
|
2502
|
-
return wrapResponse(
|
|
2697
|
+
return wrapResponse(plainValidationError(validation.issues));
|
|
2503
2698
|
ctx.body = validation.value;
|
|
2504
2699
|
return this.executeHandler(entry, ctx, finalize);
|
|
2505
2700
|
}
|
|
@@ -2508,18 +2703,18 @@ export class Server {
|
|
|
2508
2703
|
if (validation instanceof Promise) {
|
|
2509
2704
|
return validation.then((settled) => {
|
|
2510
2705
|
if (settled.issues !== undefined)
|
|
2511
|
-
return wrapResponse(
|
|
2706
|
+
return wrapResponse(plainValidationError(settled.issues));
|
|
2512
2707
|
ctx.query = settled.value;
|
|
2513
2708
|
return this.executeHandler(entry, ctx, finalize, wrapResponse);
|
|
2514
2709
|
}, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2515
2710
|
}
|
|
2516
2711
|
if (validation.issues !== undefined)
|
|
2517
|
-
return wrapResponse(
|
|
2712
|
+
return wrapResponse(plainValidationError(validation.issues));
|
|
2518
2713
|
ctx.query = validation.value;
|
|
2519
2714
|
return this.executeHandler(entry, ctx, finalize, wrapResponse);
|
|
2520
2715
|
}
|
|
2521
2716
|
}
|
|
2522
|
-
return wrapResponse(
|
|
2717
|
+
return wrapResponse(plainValidationError(originalIssues));
|
|
2523
2718
|
}
|
|
2524
2719
|
applyBodyValidation(entry, result, ctx, finalize, wrapResponse) {
|
|
2525
2720
|
if (result.issues !== undefined) {
|
|
@@ -2531,13 +2726,12 @@ export class Server {
|
|
|
2531
2726
|
}
|
|
2532
2727
|
return this.handleValidationErrorRecovery(entry, recovery, ctx, finalize, wrapResponse, result.issues, "body");
|
|
2533
2728
|
}
|
|
2534
|
-
return wrapResponse(
|
|
2729
|
+
return wrapResponse(plainValidationError(result.issues));
|
|
2535
2730
|
}
|
|
2536
2731
|
ctx.body = result.value;
|
|
2537
2732
|
return this.executeHandler(entry, ctx, finalize);
|
|
2538
2733
|
}
|
|
2539
2734
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2540
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2541
2735
|
runQueryOnly(entry, ctx, finalize, wrapResponse) {
|
|
2542
2736
|
try {
|
|
2543
2737
|
// Call the validator directly for the raw StandardResult (read `.issues`/`.value`) - skip
|
|
@@ -2574,7 +2768,7 @@ export class Server {
|
|
|
2574
2768
|
}
|
|
2575
2769
|
return this.handleValidationErrorRecovery(entry, recovery, ctx, finalize, wrapResponse, result.issues, "query");
|
|
2576
2770
|
}
|
|
2577
|
-
return wrapResponse(
|
|
2771
|
+
return wrapResponse(plainValidationError(result.issues));
|
|
2578
2772
|
}
|
|
2579
2773
|
ctx.query = result.value;
|
|
2580
2774
|
return this.executeHandler(entry, ctx, finalize, wrapResponse);
|
|
@@ -2682,333 +2876,6 @@ export class Server {
|
|
|
2682
2876
|
clearTimeout(timer);
|
|
2683
2877
|
}
|
|
2684
2878
|
}
|
|
2685
|
-
/**
|
|
2686
|
-
* The post-match lifecycle: validate → derive → beforeHandle → handler → afterHandle, with onError.
|
|
2687
|
-
* Generic over the render: a success value goes through `finalize(result, set)` (a Web `Response`, or
|
|
2688
|
-
* node-direct primitives); an early/error `Response` (validation, thrown, 500) through `wrapResponse`.
|
|
2689
|
-
*/
|
|
2690
|
-
/** Synchronous until a validator, lifecycle hook, handler, or contract check actually returns a Promise. */
|
|
2691
|
-
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2692
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2693
|
-
runLifecycle(entry, source, ctx, finalize, wrapResponse) {
|
|
2694
|
-
try {
|
|
2695
|
-
const headersSchema = entry.schema?.headers;
|
|
2696
|
-
if (headersSchema !== undefined) {
|
|
2697
|
-
const validation = headersSchema["~standard"].validate(headerObjectOf(source.headers));
|
|
2698
|
-
if (validation instanceof Promise) {
|
|
2699
|
-
return validation.then((result) => this.runLifecycleAfterHeadersResult(entry, source, ctx, finalize, wrapResponse, result), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2700
|
-
}
|
|
2701
|
-
return this.runLifecycleAfterHeadersResult(entry, source, ctx, finalize, wrapResponse, validation);
|
|
2702
|
-
}
|
|
2703
|
-
const paramsSchema = entry.schema?.params;
|
|
2704
|
-
if (paramsSchema !== undefined) {
|
|
2705
|
-
const validation = paramsSchema["~standard"].validate(ctx.params);
|
|
2706
|
-
if (validation instanceof Promise) {
|
|
2707
|
-
return validation.then((result) => this.runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, result), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2708
|
-
}
|
|
2709
|
-
return this.runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, validation);
|
|
2710
|
-
}
|
|
2711
|
-
// Registration already knows whether this route has body/params/query validation. Most real
|
|
2712
|
-
// read routes have no body or params schema, so skip the generic stage ladder and go straight to
|
|
2713
|
-
// query validation + lifecycle hooks. The fallback below remains the complete path for routes
|
|
2714
|
-
// with body/params combinations and validation recovery.
|
|
2715
|
-
if (entry.schema?.body === undefined) {
|
|
2716
|
-
return this.runQueryAndLifecycle(entry, ctx, finalize, wrapResponse);
|
|
2717
|
-
}
|
|
2718
|
-
return this.runLifecycleAfterParams(entry, source, ctx, finalize, wrapResponse, undefined);
|
|
2719
|
-
}
|
|
2720
|
-
catch (err) {
|
|
2721
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2722
|
-
}
|
|
2723
|
-
}
|
|
2724
|
-
runLifecycleAfterHeadersResult(entry, source, ctx, finalize, wrapResponse, result) {
|
|
2725
|
-
try {
|
|
2726
|
-
const headersError = this.applyLifecycleValidation(entry, result, ctx, "headers");
|
|
2727
|
-
if (headersError instanceof Promise) {
|
|
2728
|
-
return headersError.then((error) => error === undefined
|
|
2729
|
-
? this.runLifecycleAfterHeaders(entry, source, ctx, finalize, wrapResponse)
|
|
2730
|
-
: wrapResponse(error), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2731
|
-
}
|
|
2732
|
-
return headersError === undefined
|
|
2733
|
-
? this.runLifecycleAfterHeaders(entry, source, ctx, finalize, wrapResponse)
|
|
2734
|
-
: wrapResponse(headersError);
|
|
2735
|
-
}
|
|
2736
|
-
catch (err) {
|
|
2737
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2738
|
-
}
|
|
2739
|
-
}
|
|
2740
|
-
runLifecycleAfterHeaders(entry, source, ctx, finalize, wrapResponse) {
|
|
2741
|
-
const paramsSchema = entry.schema?.params;
|
|
2742
|
-
if (paramsSchema === undefined) {
|
|
2743
|
-
if (entry.schema?.body === undefined)
|
|
2744
|
-
return this.runQueryAndLifecycle(entry, ctx, finalize, wrapResponse);
|
|
2745
|
-
return this.runLifecycleAfterParams(entry, source, ctx, finalize, wrapResponse, undefined);
|
|
2746
|
-
}
|
|
2747
|
-
const validation = paramsSchema["~standard"].validate(ctx.params);
|
|
2748
|
-
if (validation instanceof Promise) {
|
|
2749
|
-
return validation.then((result) => this.runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, result), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2750
|
-
}
|
|
2751
|
-
return this.runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, validation);
|
|
2752
|
-
}
|
|
2753
|
-
/** Registration-specialized query lifecycle: query validation (including recovery) → hooks. */
|
|
2754
|
-
runLifecycleQuery(entry, ctx, finalize, wrapResponse) {
|
|
2755
|
-
try {
|
|
2756
|
-
// The execution plan only selects this runner when the route has a query schema. Keeping the
|
|
2757
|
-
// non-null access here removes the per-request schema-presence branch from realistic GETs.
|
|
2758
|
-
const validation = entry.schema.query["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
|
|
2759
|
-
if (validation instanceof Promise) {
|
|
2760
|
-
return validation.then((result) => {
|
|
2761
|
-
try {
|
|
2762
|
-
return this.runQueryAndLifecycleResult(entry, ctx, finalize, wrapResponse, result);
|
|
2763
|
-
}
|
|
2764
|
-
catch (err) {
|
|
2765
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2766
|
-
}
|
|
2767
|
-
}, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2768
|
-
}
|
|
2769
|
-
return this.runQueryAndLifecycleResult(entry, ctx, finalize, wrapResponse, validation);
|
|
2770
|
-
}
|
|
2771
|
-
catch (err) {
|
|
2772
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
/** Registration-specialized body lifecycle: bounded body validation (including recovery) → hooks. */
|
|
2776
|
-
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2777
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2778
|
-
runLifecycleBody(entry, source, ctx, finalize, wrapResponse) {
|
|
2779
|
-
return this.readAndValidateBody(source, entry, ctx).then((bodyError) => bodyError === undefined
|
|
2780
|
-
? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
2781
|
-
: wrapResponse(bodyError), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2782
|
-
}
|
|
2783
|
-
/** Registration-specialized body + query lifecycle: bounded body validation → query validation → hooks. */
|
|
2784
|
-
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2785
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: invoked structurally by compiled plans (internal/route-execution.ts)
|
|
2786
|
-
runLifecycleBodyQuery(entry, source, ctx, finalize, wrapResponse) {
|
|
2787
|
-
return this.readAndValidateBody(source, entry, ctx).then((bodyError) => bodyError === undefined
|
|
2788
|
-
? this.runLifecycleQuery(entry, ctx, finalize, wrapResponse)
|
|
2789
|
-
: wrapResponse(bodyError), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2790
|
-
}
|
|
2791
|
-
/** Registration-specialized no-body lifecycle: query validation (if present) → derives/hooks. */
|
|
2792
|
-
runQueryAndLifecycle(entry, ctx, finalize, wrapResponse) {
|
|
2793
|
-
const querySchema = entry.schema?.query;
|
|
2794
|
-
return querySchema === undefined
|
|
2795
|
-
? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
2796
|
-
: this.runLifecycleQuery(entry, ctx, finalize, wrapResponse);
|
|
2797
|
-
}
|
|
2798
|
-
runQueryAndLifecycleResult(entry, ctx, finalize, wrapResponse, result) {
|
|
2799
|
-
const queryError = this.applyLifecycleValidation(entry, result, ctx, "query");
|
|
2800
|
-
if (queryError instanceof Promise) {
|
|
2801
|
-
return queryError.then((error) => error === undefined
|
|
2802
|
-
? this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
2803
|
-
: wrapResponse(error), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2804
|
-
}
|
|
2805
|
-
if (queryError !== undefined)
|
|
2806
|
-
return wrapResponse(queryError);
|
|
2807
|
-
return this.runLifecycleHooks(entry, ctx, finalize, wrapResponse);
|
|
2808
|
-
}
|
|
2809
|
-
runLifecycleAfterParamsResult(entry, source, ctx, finalize, wrapResponse, result) {
|
|
2810
|
-
try {
|
|
2811
|
-
return this.runLifecycleAfterParams(entry, source, ctx, finalize, wrapResponse, this.applyLifecycleValidation(entry, result, ctx, "params"));
|
|
2812
|
-
}
|
|
2813
|
-
catch (err) {
|
|
2814
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
runLifecycleAfterParams(entry, source, ctx, finalize, wrapResponse, paramsError) {
|
|
2818
|
-
if (paramsError instanceof Promise) {
|
|
2819
|
-
return paramsError.then((error) => this.runLifecycleAfterParamsSettled(entry, source, ctx, finalize, wrapResponse, error), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2820
|
-
}
|
|
2821
|
-
return this.runLifecycleAfterParamsSettled(entry, source, ctx, finalize, wrapResponse, paramsError);
|
|
2822
|
-
}
|
|
2823
|
-
runLifecycleAfterParamsSettled(entry, source, ctx, finalize, wrapResponse, paramsError) {
|
|
2824
|
-
if (paramsError !== undefined)
|
|
2825
|
-
return wrapResponse(paramsError);
|
|
2826
|
-
if (entry.schema?.body !== undefined) {
|
|
2827
|
-
return this.readAndValidateBody(source, entry, ctx).then((bodyError) => this.runLifecycleAfterBody(entry, ctx, finalize, wrapResponse, bodyError), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2828
|
-
}
|
|
2829
|
-
return this.runLifecycleAfterBody(entry, ctx, finalize, wrapResponse, undefined);
|
|
2830
|
-
}
|
|
2831
|
-
runLifecycleAfterBody(entry, ctx, finalize, wrapResponse, bodyError) {
|
|
2832
|
-
if (bodyError !== undefined)
|
|
2833
|
-
return wrapResponse(bodyError);
|
|
2834
|
-
const querySchema = entry.schema?.query;
|
|
2835
|
-
if (querySchema === undefined)
|
|
2836
|
-
return this.runLifecycleHooks(entry, ctx, finalize, wrapResponse);
|
|
2837
|
-
const validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
|
|
2838
|
-
if (validation instanceof Promise) {
|
|
2839
|
-
return validation.then((result) => this.runLifecycleAfterQueryResult(entry, ctx, finalize, wrapResponse, result), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2840
|
-
}
|
|
2841
|
-
return this.runLifecycleAfterQueryResult(entry, ctx, finalize, wrapResponse, validation);
|
|
2842
|
-
}
|
|
2843
|
-
runLifecycleAfterQueryResult(entry, ctx, finalize, wrapResponse, result) {
|
|
2844
|
-
try {
|
|
2845
|
-
const queryError = this.applyLifecycleValidation(entry, result, ctx, "query");
|
|
2846
|
-
if (queryError instanceof Promise) {
|
|
2847
|
-
return queryError.then((error) => this.runLifecycleAfterQuery(entry, ctx, finalize, wrapResponse, error), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2848
|
-
}
|
|
2849
|
-
return this.runLifecycleAfterQuery(entry, ctx, finalize, wrapResponse, queryError);
|
|
2850
|
-
}
|
|
2851
|
-
catch (err) {
|
|
2852
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
2855
|
-
runLifecycleAfterQuery(entry, ctx, finalize, wrapResponse, queryError) {
|
|
2856
|
-
if (queryError !== undefined)
|
|
2857
|
-
return wrapResponse(queryError);
|
|
2858
|
-
return this.runLifecycleHooks(entry, ctx, finalize, wrapResponse);
|
|
2859
|
-
}
|
|
2860
|
-
runLifecycleHooks(entry, ctx, finalize, wrapResponse) {
|
|
2861
|
-
if (entry.lifecycleHookLane === "derive-before") {
|
|
2862
|
-
return this.runLifecycleDeriveBefore(entry, ctx, finalize, wrapResponse);
|
|
2863
|
-
}
|
|
2864
|
-
try {
|
|
2865
|
-
if (entry.hasDecorations)
|
|
2866
|
-
Object.assign(ctx, entry.decorations);
|
|
2867
|
-
for (let i = 0; i < entry.derives.length; i++) {
|
|
2868
|
-
const extension = entry.derives[i](ctx);
|
|
2869
|
-
if (extension instanceof Promise) {
|
|
2870
|
-
return this.continueLifecycleAfterDerive(entry, ctx, finalize, wrapResponse, i, extension);
|
|
2871
|
-
}
|
|
2872
|
-
Object.assign(ctx, extension);
|
|
2873
|
-
}
|
|
2874
|
-
for (let i = 0; i < entry.beforeHandle.length; i++) {
|
|
2875
|
-
const outcome = entry.beforeHandle[i](ctx);
|
|
2876
|
-
if (outcome instanceof Promise) {
|
|
2877
|
-
return this.continueLifecycleAfterBefore(entry, ctx, finalize, wrapResponse, i, outcome);
|
|
2878
|
-
}
|
|
2879
|
-
if (outcome !== undefined)
|
|
2880
|
-
return finalize(outcome, responseSet(ctx));
|
|
2881
|
-
}
|
|
2882
|
-
const result = entry.handler(ctx);
|
|
2883
|
-
if (result instanceof Promise) {
|
|
2884
|
-
return result.then((value) => this.finishLifecycleResult(entry, ctx, finalize, wrapResponse, value), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2885
|
-
}
|
|
2886
|
-
return this.finishLifecycleResult(entry, ctx, finalize, wrapResponse, result);
|
|
2887
|
-
}
|
|
2888
|
-
catch (err) {
|
|
2889
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
/** Registration-specialized derive → before → handler lifecycle. */
|
|
2893
|
-
runLifecycleDeriveBefore(entry, ctx, finalize, wrapResponse) {
|
|
2894
|
-
try {
|
|
2895
|
-
const extension = entry.derives[0](ctx);
|
|
2896
|
-
if (extension instanceof Promise) {
|
|
2897
|
-
return this.continueLifecycleAfterDerive(entry, ctx, finalize, wrapResponse, 0, extension);
|
|
2898
|
-
}
|
|
2899
|
-
Object.assign(ctx, extension);
|
|
2900
|
-
const outcome = entry.beforeHandle[0](ctx);
|
|
2901
|
-
if (outcome instanceof Promise) {
|
|
2902
|
-
return this.continueLifecycleAfterBefore(entry, ctx, finalize, wrapResponse, 0, outcome);
|
|
2903
|
-
}
|
|
2904
|
-
if (outcome !== undefined)
|
|
2905
|
-
return this.finishSimpleLifecycleResult(entry, ctx, finalize, wrapResponse, outcome);
|
|
2906
|
-
const result = entry.handler(ctx);
|
|
2907
|
-
if (result instanceof Promise) {
|
|
2908
|
-
return result.then((value) => this.finishSimpleLifecycleResult(entry, ctx, finalize, wrapResponse, value), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
2909
|
-
}
|
|
2910
|
-
return this.finishSimpleLifecycleResult(entry, ctx, finalize, wrapResponse, result);
|
|
2911
|
-
}
|
|
2912
|
-
catch (err) {
|
|
2913
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2914
|
-
}
|
|
2915
|
-
}
|
|
2916
|
-
/** Finalize the specialized lane without rechecking its registration-proven invariants. */
|
|
2917
|
-
finishSimpleLifecycleResult(entry, ctx, finalize, wrapResponse, result) {
|
|
2918
|
-
try {
|
|
2919
|
-
return finalize(result, responseSet(ctx));
|
|
2920
|
-
}
|
|
2921
|
-
catch (err) {
|
|
2922
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
async continueLifecycleAfterDerive(entry, ctx, finalize, wrapResponse, firstIndex, first) {
|
|
2926
|
-
try {
|
|
2927
|
-
Object.assign(ctx, await first);
|
|
2928
|
-
for (let i = firstIndex + 1; i < entry.derives.length; i++) {
|
|
2929
|
-
const extension = entry.derives[i](ctx);
|
|
2930
|
-
Object.assign(ctx, extension instanceof Promise ? await extension : extension);
|
|
2931
|
-
}
|
|
2932
|
-
return await this.runLifecycleHooksAsync(entry, ctx, finalize, wrapResponse);
|
|
2933
|
-
}
|
|
2934
|
-
catch (err) {
|
|
2935
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2936
|
-
}
|
|
2937
|
-
}
|
|
2938
|
-
async runLifecycleHooksAsync(entry, ctx, finalize, wrapResponse) {
|
|
2939
|
-
try {
|
|
2940
|
-
// Indexed, not `for...of`. An array iterator declared inside an async function stays live
|
|
2941
|
-
// across the `await` below, so neither engine can sink it - unlike the sync lanes above,
|
|
2942
|
-
// where V8 elides it. Measured cost of the iterator form: 43ns per hook on Bun, ~9ns on
|
|
2943
|
-
// Node, on a chain whose hooks return synchronously (the common shape).
|
|
2944
|
-
for (let i = 0; i < entry.beforeHandle.length; i++) {
|
|
2945
|
-
const outcome = entry.beforeHandle[i](ctx);
|
|
2946
|
-
const early = outcome instanceof Promise ? await outcome : outcome;
|
|
2947
|
-
if (early !== undefined)
|
|
2948
|
-
return finalize(early, responseSet(ctx));
|
|
2949
|
-
}
|
|
2950
|
-
return await this.runLifecycleHandlerAsync(entry, ctx, finalize, wrapResponse);
|
|
2951
|
-
}
|
|
2952
|
-
catch (err) {
|
|
2953
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
async continueLifecycleAfterBefore(entry, ctx, finalize, wrapResponse, firstIndex, first) {
|
|
2957
|
-
try {
|
|
2958
|
-
const firstOutcome = await first;
|
|
2959
|
-
if (firstOutcome !== undefined)
|
|
2960
|
-
return finalize(firstOutcome, responseSet(ctx));
|
|
2961
|
-
for (let i = firstIndex + 1; i < entry.beforeHandle.length; i++) {
|
|
2962
|
-
const outcome = entry.beforeHandle[i](ctx);
|
|
2963
|
-
const early = outcome instanceof Promise ? await outcome : outcome;
|
|
2964
|
-
if (early !== undefined)
|
|
2965
|
-
return finalize(early, responseSet(ctx));
|
|
2966
|
-
}
|
|
2967
|
-
return await this.runLifecycleHandlerAsync(entry, ctx, finalize, wrapResponse);
|
|
2968
|
-
}
|
|
2969
|
-
catch (err) {
|
|
2970
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2971
|
-
}
|
|
2972
|
-
}
|
|
2973
|
-
async runLifecycleHandlerAsync(entry, ctx, finalize, wrapResponse) {
|
|
2974
|
-
try {
|
|
2975
|
-
const output = entry.handler(ctx);
|
|
2976
|
-
const result = output instanceof Promise ? await output : output;
|
|
2977
|
-
return await this.finishLifecycleResult(entry, ctx, finalize, wrapResponse, result);
|
|
2978
|
-
}
|
|
2979
|
-
catch (err) {
|
|
2980
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2981
|
-
}
|
|
2982
|
-
}
|
|
2983
|
-
finishLifecycleResult(entry, ctx, finalize, wrapResponse, initial) {
|
|
2984
|
-
try {
|
|
2985
|
-
let result = initial;
|
|
2986
|
-
for (let i = 0; i < entry.afterHandle.length; i++) {
|
|
2987
|
-
const transformed = entry.afterHandle[i](result, ctx);
|
|
2988
|
-
if (transformed instanceof Promise) {
|
|
2989
|
-
return this.continueLifecycleAfterHandle(entry, ctx, finalize, wrapResponse, i, transformed);
|
|
2990
|
-
}
|
|
2991
|
-
result = transformed;
|
|
2992
|
-
}
|
|
2993
|
-
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result);
|
|
2994
|
-
}
|
|
2995
|
-
catch (err) {
|
|
2996
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
async continueLifecycleAfterHandle(entry, ctx, finalize, wrapResponse, firstIndex, first) {
|
|
3000
|
-
try {
|
|
3001
|
-
let result = await first;
|
|
3002
|
-
for (let i = firstIndex + 1; i < entry.afterHandle.length; i++) {
|
|
3003
|
-
const transformed = entry.afterHandle[i](result, ctx);
|
|
3004
|
-
result = transformed instanceof Promise ? await transformed : transformed;
|
|
3005
|
-
}
|
|
3006
|
-
return await this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, result);
|
|
3007
|
-
}
|
|
3008
|
-
catch (err) {
|
|
3009
|
-
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
2879
|
finishLifecycleContract(entry, ctx, finalize, wrapResponse, result) {
|
|
3013
2880
|
try {
|
|
3014
2881
|
const contract = entry.responseContract;
|
|
@@ -3031,7 +2898,7 @@ export class Server {
|
|
|
3031
2898
|
path: pathnameOf(ctx.req.url),
|
|
3032
2899
|
detail: outcome.message,
|
|
3033
2900
|
});
|
|
3034
|
-
return wrapResponse(
|
|
2901
|
+
return wrapResponse(plainError(500, "internal_error"));
|
|
3035
2902
|
}
|
|
3036
2903
|
if (outcome.kind === "warn") {
|
|
3037
2904
|
this.logger.warn("response contract", {
|
|
@@ -3042,14 +2909,33 @@ export class Server {
|
|
|
3042
2909
|
}
|
|
3043
2910
|
return finalize(outcome.value, responseSet(ctx));
|
|
3044
2911
|
}
|
|
3045
|
-
|
|
2912
|
+
/** Synchronous by design, and `MaybePromise` rather than `Promise` for the same reason: control flow
|
|
2913
|
+
* out of a lifecycle stage must not cost a promise. An `async` method allocates one and defers the
|
|
2914
|
+
* result a microtask even when every branch it takes returns immediately, which is the common case
|
|
2915
|
+
* here - a thrown `status(...)`/`Response`, or a route with no `onError` hook at all. Only the hook
|
|
2916
|
+
* loop, which may await, drops into {@link runErrorHooks}. */
|
|
2917
|
+
handleLifecycleError(entry, err, ctx, finalize, wrapResponse) {
|
|
3046
2918
|
// A *thrown* Response is deliberate control flow, not an error - a guard throws a redirect/401,
|
|
3047
2919
|
// an action throws an error page. Return it as-is (Remix/SvelteKit semantics); don't run onError
|
|
3048
2920
|
// or log it as a 500. This is what makes `throw redirect(...)` / `requireSession(...)` work from
|
|
3049
2921
|
// any handler or loader.
|
|
3050
2922
|
if (err instanceof Response)
|
|
3051
2923
|
return wrapResponse(err);
|
|
3052
|
-
//
|
|
2924
|
+
// Same rule for a thrown `status(...)`, but through `finalize` rather than `wrapResponse`: the
|
|
2925
|
+
// value is still plain data, so the ordinary JSON lane renders it and no `Response` is built.
|
|
2926
|
+
if (isResponseResult(err))
|
|
2927
|
+
return finalize(err, responseSet(ctx));
|
|
2928
|
+
if (entry.onError.length === 0) {
|
|
2929
|
+
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
2930
|
+
// (redacting) logger. Body-read failures and around-hook failures land here too.
|
|
2931
|
+
this.logRequestError(err, ctx);
|
|
2932
|
+
return wrapResponse(plainError(500, "internal_error"));
|
|
2933
|
+
}
|
|
2934
|
+
return this.runErrorHooks(entry, err, ctx, finalize, wrapResponse);
|
|
2935
|
+
}
|
|
2936
|
+
/** The `onError` chain, split off {@link handleLifecycleError} so its `await` costs only the routes
|
|
2937
|
+
* that registered a hook. A hook may return a custom response; otherwise the default 500 stands. */
|
|
2938
|
+
async runErrorHooks(entry, err, ctx, finalize, wrapResponse) {
|
|
3053
2939
|
// Indexed for the same reason as the `beforeHandle` chain above: the iterator would outlive
|
|
3054
2940
|
// the `await` and stay heap-allocated on both engines.
|
|
3055
2941
|
for (let i = 0; i < entry.onError.length; i++) {
|
|
@@ -3058,53 +2944,26 @@ export class Server {
|
|
|
3058
2944
|
if (handled !== undefined)
|
|
3059
2945
|
return finalize(handled, responseSet(ctx));
|
|
3060
2946
|
}
|
|
3061
|
-
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
3062
|
-
// (redacting) logger. Body-read failures and around-hook failures land here too.
|
|
3063
2947
|
this.logRequestError(err, ctx);
|
|
3064
|
-
return wrapResponse(
|
|
2948
|
+
return wrapResponse(plainError(500, "internal_error"));
|
|
3065
2949
|
}
|
|
3066
|
-
/** Log an unhandled request error to the (redacting) logger - shared by
|
|
2950
|
+
/** Log an unhandled request error to the (redacting) logger - shared by the compiled route program and the
|
|
3067
2951
|
* bare fast path ({@link bareError}) so both record the same fields. Never throws; never leaks. */
|
|
3068
2952
|
logRequestError(err, ctx) {
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
// the
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
});
|
|
3084
|
-
}
|
|
3085
|
-
async readAndValidateBody(req, entry, ctx) {
|
|
3086
|
-
const contentType = headerOf(req, "content-type") ?? "";
|
|
3087
|
-
let parsed;
|
|
3088
|
-
if (contentType === "application/json" || contentType.includes("application/json")) {
|
|
3089
|
-
const json = await this.readBoundedJson(req, entry.bodyLimit ?? this.maxBodyBytes);
|
|
3090
|
-
if (json instanceof Response)
|
|
3091
|
-
return json;
|
|
3092
|
-
parsed = json;
|
|
3093
|
-
}
|
|
3094
|
-
else if (isUrlEncodedForm(contentType)) {
|
|
3095
|
-
const form = await readBoundedForm(req, entry.bodyLimit ?? this.maxBodyBytes);
|
|
3096
|
-
if (form instanceof Response)
|
|
3097
|
-
return form;
|
|
3098
|
-
parsed = form;
|
|
3099
|
-
}
|
|
3100
|
-
else {
|
|
3101
|
-
// multipart/form-data (file uploads) stays 415 on the schema path by design - files don't
|
|
3102
|
-
// fit a value schema; use a schema-less route + @nifrajs/uploads helpers for those.
|
|
3103
|
-
return jsonError(415, "unsupported_media_type");
|
|
3104
|
-
}
|
|
3105
|
-
const validation = entry.schema.body["~standard"].validate(parsed);
|
|
3106
|
-
const result = validation instanceof Promise ? await validation : validation;
|
|
3107
|
-
return this.applyLifecycleValidation(entry, result, ctx, "body");
|
|
2953
|
+
emitRequestErrorLog(this.logger, this.errorLogDetail, err, ctx);
|
|
2954
|
+
}
|
|
2955
|
+
readAndValidateBody(req, entry, ctx, bodySchema = entry.schema?.body, bodyLimit = entry.bodyLimit) {
|
|
2956
|
+
// Keep the generic lifecycle body stage on the same framed parser/continuation as the body-only
|
|
2957
|
+
// lane. This avoids an async wrapper that first awaits parsing and then starts validation in a
|
|
2958
|
+
// second continuation, while retaining the exact bounded-read, content-type, and poisoning
|
|
2959
|
+
// contracts shared by every body route.
|
|
2960
|
+
const validate = (parsed) => {
|
|
2961
|
+
const validation = bodySchema["~standard"].validate(parsed);
|
|
2962
|
+
return validation instanceof Promise
|
|
2963
|
+
? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, "body"))
|
|
2964
|
+
: this.applyLifecycleValidation(entry, validation, ctx, "body");
|
|
2965
|
+
};
|
|
2966
|
+
return readBodyFramed(req, bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, validate, (response) => response, (error) => Promise.reject(error));
|
|
3108
2967
|
}
|
|
3109
2968
|
/** Apply validation and its recovery hook on the generic lifecycle lane. Recovery is completed
|
|
3110
2969
|
* before derives/beforeHandle run, matching the body-only and query-only execution lanes. */
|
|
@@ -3125,7 +2984,7 @@ export class Server {
|
|
|
3125
2984
|
}
|
|
3126
2985
|
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError;
|
|
3127
2986
|
if (hook === undefined)
|
|
3128
|
-
return
|
|
2987
|
+
return plainValidationError(result.issues);
|
|
3129
2988
|
const attempted = hook(result.issues, ctx, kind);
|
|
3130
2989
|
if (attempted instanceof Promise) {
|
|
3131
2990
|
return attempted.then((recovery) => this.finishLifecycleValidationRecovery(entry, kind, result.issues, recovery, assign));
|
|
@@ -3134,7 +2993,7 @@ export class Server {
|
|
|
3134
2993
|
}
|
|
3135
2994
|
finishLifecycleValidationRecovery(entry, kind, issues, recovery, assign) {
|
|
3136
2995
|
if (recovery === undefined)
|
|
3137
|
-
return
|
|
2996
|
+
return plainValidationError(issues);
|
|
3138
2997
|
if (recovery instanceof Response)
|
|
3139
2998
|
return recovery;
|
|
3140
2999
|
const schema = kind === "body"
|
|
@@ -3148,23 +3007,16 @@ export class Server {
|
|
|
3148
3007
|
if (retried instanceof Promise) {
|
|
3149
3008
|
return retried.then((settled) => {
|
|
3150
3009
|
if (settled.issues !== undefined)
|
|
3151
|
-
return
|
|
3010
|
+
return plainValidationError(settled.issues);
|
|
3152
3011
|
assign(settled.value);
|
|
3153
3012
|
return undefined;
|
|
3154
3013
|
});
|
|
3155
3014
|
}
|
|
3156
3015
|
if (retried.issues !== undefined)
|
|
3157
|
-
return
|
|
3016
|
+
return plainValidationError(retried.issues);
|
|
3158
3017
|
assign(retried.value);
|
|
3159
3018
|
return undefined;
|
|
3160
3019
|
}
|
|
3161
|
-
readBoundedJson(req, maxBodyBytes = this.maxBodyBytes, onResult, onError) {
|
|
3162
|
-
// Not `async`: this is a pass-through, and wrapping the lane's own promise in a coroutine
|
|
3163
|
-
// frame costs an extra microtask hop on every JSON body.
|
|
3164
|
-
return onResult === undefined
|
|
3165
|
-
? readBoundedJsonSource(req, maxBodyBytes, this.protoPoisoning)
|
|
3166
|
-
: readBoundedJsonSource(req, maxBodyBytes, this.protoPoisoning, onResult, onError);
|
|
3167
|
-
}
|
|
3168
3020
|
/** Adapt one route's compiled execution plan to Bun's already-matched request shape. Route
|
|
3169
3021
|
* semantics remain in the plan; this closure only supplies native params and deadline fallback. */
|
|
3170
3022
|
compileBunNativeHandler(entry, paramNames, fused, signal, budget) {
|
|
@@ -3328,9 +3180,12 @@ export class Server {
|
|
|
3328
3180
|
// matched route's handler, set by `server.upgrade`.
|
|
3329
3181
|
// With WS routes, the dispatcher comes from the installed `.use(websocket())` runtime - non-null
|
|
3330
3182
|
// because wsRouteCount > 0 means ws() ran, and ws() requires the runtime at registration.
|
|
3183
|
+
// Native pub/sub when the app has WS routes and none validate outbound frames: `ws.subscribe` and
|
|
3184
|
+
// `app.publish` go through Bun's own (uWebSockets) broadcast instead of the JS registry loop.
|
|
3185
|
+
const nativePubsub = this.wsRouteCount > 0 && !this.wsHasValidatedSend;
|
|
3331
3186
|
const wsHandlers = this.wsRouteCount === 0
|
|
3332
3187
|
? undefined
|
|
3333
|
-
: this.wsRuntime.bunHandlers(this.topics);
|
|
3188
|
+
: this.wsRuntime.bunHandlers(this.topics, nativePubsub);
|
|
3334
3189
|
const reusePort = options?.reusePort === true;
|
|
3335
3190
|
// Spread rather than pass `hostname: undefined` - Bun treats an explicit undefined as a value
|
|
3336
3191
|
// on some option paths, and omitting is what selects its 0.0.0.0 default.
|
|
@@ -3366,6 +3221,14 @@ export class Server {
|
|
|
3366
3221
|
},
|
|
3367
3222
|
}));
|
|
3368
3223
|
this.bunServer = running;
|
|
3224
|
+
// Bind `app.publish` to Bun's native broadcast now that the server handle exists. Guarded on the
|
|
3225
|
+
// method's presence so a runtime whose handle lacks it simply keeps the registry path.
|
|
3226
|
+
if (nativePubsub && typeof running.publish === "function") {
|
|
3227
|
+
const native = running.publish.bind(running);
|
|
3228
|
+
this.nativePublish = (topic, data) => {
|
|
3229
|
+
native(topic, data);
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3369
3232
|
this.sealed = true;
|
|
3370
3233
|
if (this.gracefulSignals)
|
|
3371
3234
|
this.installSignalHandlers();
|