@nifrajs/core 4.0.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/assurance.d.ts +86 -6
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +186 -95
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +8 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +53 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/durable-effect.js +1 -1
- package/dist/durable-effect.js.map +1 -1
- package/dist/durable-saga.d.ts +6 -0
- package/dist/durable-saga.d.ts.map +1 -1
- package/dist/durable-saga.js +83 -10
- package/dist/durable-saga.js.map +1 -1
- package/dist/edge-kit.d.ts +8 -3
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +7 -2
- package/dist/edge-kit.js.map +1 -1
- package/dist/effect-scope.d.ts.map +1 -1
- package/dist/effect-scope.js +16 -5
- package/dist/effect-scope.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +9 -1
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/durable-shared.d.ts +1 -2
- package/dist/internal/durable-shared.d.ts.map +1 -1
- package/dist/internal/durable-shared.js +25 -44
- package/dist/internal/durable-shared.js.map +1 -1
- package/dist/internal/durable-types.d.ts +3 -0
- package/dist/internal/durable-types.d.ts.map +1 -1
- package/dist/internal/hook-origins.d.ts +67 -0
- package/dist/internal/hook-origins.d.ts.map +1 -0
- package/dist/internal/hook-origins.js +86 -0
- package/dist/internal/hook-origins.js.map +1 -0
- package/dist/internal/route-assurance.d.ts +6 -3
- package/dist/internal/route-assurance.d.ts.map +1 -1
- package/dist/internal/route-assurance.js +29 -14
- package/dist/internal/route-assurance.js.map +1 -1
- package/dist/internal/route-compiler.d.ts +9 -0
- package/dist/internal/route-compiler.d.ts.map +1 -1
- package/dist/internal/route-compiler.js +95 -13
- package/dist/internal/route-compiler.js.map +1 -1
- package/dist/internal/route-program.d.ts +1 -1
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +59 -16
- package/dist/internal/route-program.js.map +1 -1
- package/dist/internal/same-origin.d.ts +11 -1
- package/dist/internal/same-origin.d.ts.map +1 -1
- package/dist/internal/same-origin.js +8 -0
- package/dist/internal/same-origin.js.map +1 -1
- package/dist/reconciliation-worker.d.ts.map +1 -1
- package/dist/reconciliation-worker.js +12 -2
- package/dist/reconciliation-worker.js.map +1 -1
- package/dist/reflection.d.ts +53 -2
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +352 -5
- package/dist/reflection.js.map +1 -1
- package/dist/router/pattern.d.ts +1 -1
- package/dist/router/pattern.d.ts.map +1 -1
- package/dist/router/pattern.js +6 -3
- package/dist/router/pattern.js.map +1 -1
- package/dist/security/baseline.d.ts +3 -0
- package/dist/security/baseline.d.ts.map +1 -1
- package/dist/security/baseline.js +9 -5
- package/dist/security/baseline.js.map +1 -1
- package/dist/server/auth.d.ts +32 -3
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +23 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/body-lane.d.ts +8 -2
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +27 -2
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +34 -1
- package/dist/server/body.js.map +1 -1
- package/dist/server/client-ip.d.ts +3 -0
- package/dist/server/client-ip.d.ts.map +1 -1
- package/dist/server/client-ip.js +6 -5
- package/dist/server/client-ip.js.map +1 -1
- package/dist/server/context.d.ts +2 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +13 -1
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/edge.d.ts.map +1 -1
- package/dist/server/edge.js +2 -1
- package/dist/server/edge.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +5 -1
- package/dist/server/http.js.map +1 -1
- package/dist/server/idempotency-lane.d.ts +6 -3
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +38 -17
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/install.d.ts +2 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +2 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logger.js +3 -1
- package/dist/server/logger.js.map +1 -1
- package/dist/server/methods.d.ts +1 -0
- package/dist/server/methods.d.ts.map +1 -1
- package/dist/server/methods.js +1 -0
- package/dist/server/methods.js.map +1 -1
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +29 -11
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/platform-adapters.d.ts.map +1 -1
- package/dist/server/platform-adapters.js +3 -2
- package/dist/server/platform-adapters.js.map +1 -1
- package/dist/server/request-context.d.ts +2 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +4 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +44 -11
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +192 -79
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +21 -6
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +139 -14
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts.map +1 -1
- package/dist/server/response-observer-runtime.js +10 -41
- package/dist/server/response-observer-runtime.js.map +1 -1
- package/dist/server/runtime-core.d.ts +2 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +2 -0
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +45 -10
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +67 -32
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +481 -334
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +11 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +18 -3
- package/dist/server/sse.js.map +1 -1
- package/dist/server/transport-lane.d.ts +7 -1
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/server/webhook.d.ts +5 -2
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +17 -1
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +33 -14
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +47 -12
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +7 -1
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +42 -2
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +15 -3
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +4 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts +6 -0
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +55 -6
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +71 -40
- package/dist/tool-contract.js.map +1 -1
- package/dist/wire.d.ts +5 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +5 -0
- package/dist/wire.js.map +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +4 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/assurance.ts +299 -111
- package/src/capabilities.ts +62 -1
- package/src/durable-effect.ts +1 -1
- package/src/durable-saga.ts +94 -10
- package/src/edge-kit.ts +8 -3
- package/src/effect-scope.ts +15 -5
- package/src/errors.ts +1 -0
- package/src/evidence.ts +12 -3
- package/src/index.ts +7 -2
- package/src/internal/durable-shared.ts +25 -37
- package/src/internal/durable-types.ts +3 -0
- package/src/internal/hook-origins.ts +153 -0
- package/src/internal/route-assurance.ts +32 -18
- package/src/internal/route-compiler.ts +107 -16
- package/src/internal/route-program.ts +98 -44
- package/src/internal/same-origin.ts +14 -1
- package/src/reconciliation-worker.ts +10 -2
- package/src/reflection.ts +455 -4
- package/src/router/pattern.ts +6 -3
- package/src/security/baseline.ts +10 -5
- package/src/server/auth.ts +45 -3
- package/src/server/body-lane.ts +36 -1
- package/src/server/body.ts +34 -1
- package/src/server/client-ip.ts +8 -5
- package/src/server/context.ts +2 -1
- package/src/server/cookies.ts +12 -1
- package/src/server/edge.ts +2 -1
- package/src/server/http.ts +7 -1
- package/src/server/idempotency-lane.ts +64 -22
- package/src/server/install.ts +3 -0
- package/src/server/logger.ts +3 -1
- package/src/server/methods.ts +1 -0
- package/src/server/node-outcome.ts +34 -12
- package/src/server/platform-adapters.ts +3 -2
- package/src/server/request-context.ts +5 -0
- package/src/server/respond.ts +222 -69
- package/src/server/response-contract-lane.ts +162 -10
- package/src/server/response-observer-runtime.ts +16 -38
- package/src/server/runtime-core.ts +2 -0
- package/src/server/server-types.ts +45 -10
- package/src/server/server.ts +707 -539
- package/src/server/sse.ts +84 -55
- package/src/server/transport-lane.ts +7 -1
- package/src/server/webhook.ts +22 -3
- package/src/server/websocket.ts +83 -18
- package/src/server/ws-bun.ts +48 -1
- package/src/server/ws-hook.ts +14 -2
- package/src/server.ts +6 -1
- package/src/single-copy.ts +55 -5
- package/src/tool-contract.ts +67 -39
- package/src/wire.ts +14 -0
- package/src/ws.ts +6 -0
package/dist/server/server.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { admitDeadline, createRequestBudget, createUnboundedRequestBudget, NIFRA_DEADLINE_HEADER, } from "../budget.js";
|
|
2
2
|
import { FrameworkError, RouteConfigError } from "../errors.js";
|
|
3
3
|
import { DEFAULT_CAPABILITY_INTERCEPTOR_TIMEOUT_MS, } from "../internal/capability-runtime.js";
|
|
4
|
+
import { adoptStaticTags, enterPlugin, leavePlugin, tagAdoption, tagHook, tagScoped, tagStatic, } from "../internal/hook-origins.js";
|
|
4
5
|
import { assuranceDeclarationsOf, assuranceEvidenceFor, } from "../internal/route-assurance.js";
|
|
5
6
|
import { RouteCatalog } from "../internal/route-catalog.js";
|
|
6
|
-
import { compileRouteOptions } from "../internal/route-compiler.js";
|
|
7
|
+
import { adoptRouteRuntimes, compileRouteOptions } from "../internal/route-compiler.js";
|
|
7
8
|
import { compileRouteExecutionPlan, } from "../internal/route-execution.js";
|
|
8
9
|
import { compileRouteProgram, executeRouteProgram } from "../internal/route-program.js";
|
|
9
10
|
import { isSameOriginRequest } from "../internal/same-origin.js";
|
|
@@ -21,12 +22,12 @@ import { queryObjectOf, searchOf } from "./query.js";
|
|
|
21
22
|
import { PLATFORM_PEER, RequestContext, readBodyFramed, } from "./request-context.js";
|
|
22
23
|
import { recordRequestReplacement } from "./request-lineage.js";
|
|
23
24
|
import { hasDotSegment, resolveDotSegments } from "./request-target.js";
|
|
24
|
-
import { applyStaticResponseHeaders, buildStaticResponseHeaders, fusedRespond, fusedRespondNoSet, toResponse, } from "./respond.js";
|
|
25
|
-
import { CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, } from "./runtime-core.js";
|
|
25
|
+
import { applyStaticResponseHeaders, buildStaticResponseHeaders, fusedRespond, fusedRespondNoSet, toResponse, writableResponse, } from "./respond.js";
|
|
26
|
+
import { CONTEXT_REQUEST, CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, } from "./runtime-core.js";
|
|
26
27
|
import { normalizeStaticResponseHeaders } from "./static-headers.js";
|
|
27
28
|
import { plainValidationError } from "./validation.js";
|
|
28
29
|
import { NIFRA_BACKEND_WS_RUNTIME } from "../mount.js";
|
|
29
|
-
import { GET_WS_RUNTIME, INSTALL_EFFECT_LEDGER, INSTALL_IDEMPOTENCY, INSTALL_MCP, INSTALL_NODE_DIRECT, INSTALL_NOT_FOUND, INSTALL_RESPONSE_CONTRACT, INSTALL_RESPONSE_OBSERVER, INSTALL_SSE, INSTALL_WS, NODE_NATIVE_MOUNT, RESOLVE_NODE_MOUNT, } from "./install.js";
|
|
30
|
+
import { GET_WS_RUNTIME, INSTALL_EFFECT_LEDGER, INSTALL_IDEMPOTENCY, INSTALL_MCP, INSTALL_NODE_DIRECT, INSTALL_NOT_FOUND, INSTALL_RESPONSE_CONTRACT, INSTALL_RESPONSE_OBSERVER, INSTALL_SSE, INSTALL_WS, NODE_NATIVE_MOUNT, REPORT_ERROR, RESOLVE_NODE_MOUNT, } from "./install.js";
|
|
30
31
|
import { jsonLogger } from "./logger.js";
|
|
31
32
|
// CORS uses this marker to say its request hook only handles OPTIONS preflight. Bun can keep
|
|
32
33
|
// ordinary registered routes on its native table and send unmatched OPTIONS requests through the
|
|
@@ -214,11 +215,16 @@ const underPrefix = (prefix) => {
|
|
|
214
215
|
};
|
|
215
216
|
};
|
|
216
217
|
/** A merged server's hook scope: exactly the routes MERGED, snapshotted into a dedicated matcher so
|
|
217
|
-
* the guard never reflects whatever the source's own catalog grows into afterwards.
|
|
218
|
+
* the guard never reflects whatever the source's own catalog grows into afterwards. An `OPTIONS`
|
|
219
|
+
* request to a path they serve is in scope too: a CORS preflight names the path, not a route, and
|
|
220
|
+
* the group's `cors()` is the only hook that can answer it. */
|
|
218
221
|
const servedBy = (routes) => {
|
|
219
222
|
const scope = new RouteCatalog();
|
|
220
223
|
scope.addBatch(routes);
|
|
221
|
-
return (req) =>
|
|
224
|
+
return (req) => {
|
|
225
|
+
const match = scope.find(req.method, pathnameOf(req.url));
|
|
226
|
+
return match.found || (req.method === "OPTIONS" && match.reason === "method-not-allowed");
|
|
227
|
+
};
|
|
222
228
|
};
|
|
223
229
|
/** Gate adopted hooks to `inScope` (`undefined`: adopt them unchanged). The request is a request
|
|
224
230
|
* hook's first argument (`reqAt` 0) and every response-side hook's second; out of scope a hook
|
|
@@ -305,30 +311,67 @@ const captureCallerSite = () => {
|
|
|
305
311
|
return undefined;
|
|
306
312
|
};
|
|
307
313
|
const hookAudit = /* @__PURE__ */ new WeakMap();
|
|
314
|
+
/** Whether `from` reaches `target` through mounts that keep the path and take a request under `path`.
|
|
315
|
+
* Module-level, not a `Server` method, so a production bundle drops it with its dev-only caller. */
|
|
316
|
+
function mountLeadsBack(from, target, path, mountsOf, seen) {
|
|
317
|
+
if (from === target)
|
|
318
|
+
return true;
|
|
319
|
+
if (!(from instanceof Server) || seen.has(from))
|
|
320
|
+
return false;
|
|
321
|
+
seen.add(from);
|
|
322
|
+
const under = (prefix) => (prefix === "/" ? "/" : `${prefix}/`);
|
|
323
|
+
return mountsOf(from).some((mount) => !mount.stripPrefix &&
|
|
324
|
+
(under(path).startsWith(under(mount.path)) || under(mount.path).startsWith(under(path))) &&
|
|
325
|
+
mountLeadsBack(mount.app, target, path, mountsOf, seen));
|
|
326
|
+
}
|
|
327
|
+
/** Set by `@nifrajs/web`'s `createWebApp` on its app once the page routes are declared. */
|
|
328
|
+
const PAGES_DECLARED = /* @__PURE__ */ Symbol.for("nifra.web.pagesDeclared");
|
|
308
329
|
/** Start tracking one server's order-scoped hook pushes (once, from the constructor). */
|
|
309
330
|
const beginHookAudit = (key, policy, hasCustomLogger) => {
|
|
310
331
|
hookAudit.set(key, { policy, hasCustomLogger, sites: [], checked: false });
|
|
311
332
|
};
|
|
312
333
|
/** Record one order-scoped hook push: the route count at that moment (so `routesAtPush === total` at
|
|
313
|
-
* seal means no route was declared after it) and the caller's frame.
|
|
334
|
+
* seal means no route was declared after it) and the caller's frame. Hooks a WebSocket handshake runs
|
|
335
|
+
* count sockets as routes; `around` and `afterHandle` wrap a response, so they count HTTP routes only. */
|
|
314
336
|
const recordScopedHook = (key, routesAtPush, kind) => {
|
|
337
|
+
// Not a dead hook when routes follow it, but every page already copied its chain without it.
|
|
338
|
+
if (PAGES_DECLARED in key) {
|
|
339
|
+
const site = captureCallerSite();
|
|
340
|
+
throw new FrameworkError("HOOK_AFTER_PAGES", `${kind}()${site === undefined ? "" : ` at ${site}`} was added after createWebApp declared the page routes, so no page runs it: each route keeps the order-scoped hooks that existed when it was declared. Apply it in createWebApp({ use: (app) => ... }) to cover the pages, or inside app.group(prefix, ...) to scope it to the routes declared there.`);
|
|
341
|
+
}
|
|
315
342
|
const state = hookAudit.get(key);
|
|
316
343
|
if (state === undefined || state.policy === "off")
|
|
317
344
|
return;
|
|
318
345
|
state.sites.push({ kind, routesAtPush, site: captureCallerSite() });
|
|
319
346
|
};
|
|
347
|
+
/** Hook provenance for an adoption (dev only, see `internal/hook-origins.ts`): which scope each
|
|
348
|
+
* wrapper just appended to a hook list gates its hook by. `pairs` holds each source list with the target list it
|
|
349
|
+
* was appended to, read right after the append so the wrappers are the target list's tail. */
|
|
350
|
+
const tagAdopted = (target, source, prefix, routes, pairs) => {
|
|
351
|
+
tagAdoption(target, source, prefix);
|
|
352
|
+
const scope = prefix !== undefined
|
|
353
|
+
? { prefix }
|
|
354
|
+
: { routes: routes.map((route) => ({ method: route.method, path: route.path })) };
|
|
355
|
+
for (const [originals, appended] of pairs) {
|
|
356
|
+
if (originals.length > 0)
|
|
357
|
+
tagScoped(originals, appended.slice(-originals.length), scope);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
320
360
|
/** Run the deadness check exactly once. A hook whose recorded route count equals the final route count
|
|
321
361
|
* had no route declared after it - unambiguously dead, since the scoping feature (a hook that covers
|
|
322
362
|
* only later routes) always leaves at least one such route. Zero-route apps are exempt (different
|
|
323
363
|
* mistake, and the warning would be noise). */
|
|
324
|
-
const sealHookAudit = (key, total, logger) => {
|
|
364
|
+
const sealHookAudit = (key, total, sockets, logger) => {
|
|
325
365
|
const state = hookAudit.get(key);
|
|
326
366
|
if (state === undefined || state.checked)
|
|
327
367
|
return;
|
|
328
368
|
state.checked = true;
|
|
329
|
-
if (state.policy === "off" || total === 0)
|
|
369
|
+
if (state.policy === "off" || total + sockets === 0)
|
|
330
370
|
return;
|
|
331
|
-
const dead = state.sites.filter((hook) => hook.routesAtPush ===
|
|
371
|
+
const dead = state.sites.filter((hook) => hook.routesAtPush ===
|
|
372
|
+
(hook.kind === "around" || hook.kind === "aroundCapability" || hook.kind === "afterHandle"
|
|
373
|
+
? total
|
|
374
|
+
: total + sockets));
|
|
332
375
|
if (dead.length === 0)
|
|
333
376
|
return;
|
|
334
377
|
const detail = dead.map((hook) => ` ${hook.kind}() at ${hook.site ?? "<unknown>"}`).join("\n");
|
|
@@ -355,7 +398,9 @@ export class Server {
|
|
|
355
398
|
catalog;
|
|
356
399
|
/** WebSocket routes, matched separately at upgrade time (a GET + `Upgrade: websocket`). */
|
|
357
400
|
wsRouter;
|
|
358
|
-
|
|
401
|
+
/** Each socket path, its declared capabilities, the evidence declarations its handshake captured
|
|
402
|
+
* and the chain it runs; reflection reads it. */
|
|
403
|
+
wsRoutes;
|
|
359
404
|
/** `wsChanges` when `wsMountsLive` was last worked out; -1 until then. */
|
|
360
405
|
wsMountsCheckedAt = -1;
|
|
361
406
|
/** Whether an app mounted here takes WebSocket upgrades, as of `wsMountsCheckedAt`. */
|
|
@@ -374,6 +419,7 @@ export class Server {
|
|
|
374
419
|
nativePublish;
|
|
375
420
|
maxBodyBytes;
|
|
376
421
|
protoPoisoning;
|
|
422
|
+
formOrigins;
|
|
377
423
|
/** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
|
|
378
424
|
trustBodyFraming;
|
|
379
425
|
wsMaxPayloadBytes;
|
|
@@ -463,8 +509,13 @@ export class Server {
|
|
|
463
509
|
/** Body/raw response hooks need a framework-payload marker; keep that decision per app. */
|
|
464
510
|
responseBodyTag;
|
|
465
511
|
responseBodyOwners;
|
|
466
|
-
finalizeResponse = (result, set) => toResponse(result, set, this.responseBodyTag, this.staticResponseHeaders
|
|
512
|
+
finalizeResponse = (result, set, ctx) => toResponse(result, set, this.responseBodyTag, this.staticResponseHeaders,
|
|
513
|
+
// Response hooks write next, and they run for this same request object.
|
|
514
|
+
ctx !== undefined && this.onResponseHooks.length > 0 ? ctx[CONTEXT_REQUEST] : undefined);
|
|
467
515
|
responseRequests;
|
|
516
|
+
/** A request the WebSocket handshake check ran the onRequest hooks on and then passed to HTTP →
|
|
517
|
+
* what the hooks returned, so `fetch` routes that instead of running the hooks a second time. */
|
|
518
|
+
wsHookedPasses;
|
|
468
519
|
/** Original source → request observed by generic request hooks, including in-place mutations. */
|
|
469
520
|
responseSources;
|
|
470
521
|
/** Memoized NodeRequestContext per plain-`Request` source - see {@link nodeRequestContextOf}. */
|
|
@@ -488,11 +539,12 @@ export class Server {
|
|
|
488
539
|
this.routePrefix = "";
|
|
489
540
|
this.catalog = new RouteCatalog();
|
|
490
541
|
this.wsRouter = new Router();
|
|
491
|
-
this.
|
|
542
|
+
this.wsRoutes = [];
|
|
492
543
|
this.topics = undefined;
|
|
493
544
|
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
494
545
|
assertByteLimit(maxBodyBytes, "maxBodyBytes");
|
|
495
546
|
this.protoPoisoning = options.protoPoisoning ?? "reject";
|
|
547
|
+
this.formOrigins = options.formOrigins;
|
|
496
548
|
this.trustBodyFraming = options.trustBodyFraming ?? false;
|
|
497
549
|
const wsMaxPayloadBytes = options.wsMaxPayloadBytes ?? maxBodyBytes;
|
|
498
550
|
assertByteLimit(wsMaxPayloadBytes, "wsMaxPayloadBytes");
|
|
@@ -500,7 +552,13 @@ export class Server {
|
|
|
500
552
|
this.wsMaxPayloadBytes = wsMaxPayloadBytes;
|
|
501
553
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 0;
|
|
502
554
|
this.wsUpgradeTimeoutMs = options.wsUpgradeTimeoutMs ?? DEFAULT_WS_UPGRADE_TIMEOUT_MS;
|
|
503
|
-
|
|
555
|
+
const trust = options.clientIp;
|
|
556
|
+
if (trust !== undefined &&
|
|
557
|
+
!("header" in trust) &&
|
|
558
|
+
!(Number.isSafeInteger(trust.trustedHops) && trust.trustedHops >= 0)) {
|
|
559
|
+
throw new RangeError("clientIp trustedHops must be a non-negative integer");
|
|
560
|
+
}
|
|
561
|
+
this.clientIpTrust = trust;
|
|
504
562
|
this.acceptInboundDeadlines = options.acceptInboundDeadlines ?? false;
|
|
505
563
|
this.maxInboundDeadlineMs = options.maxInboundDeadlineMs ?? 30_000;
|
|
506
564
|
this.deadlineAdmissionOptions = Object.freeze({
|
|
@@ -516,7 +574,8 @@ export class Server {
|
|
|
516
574
|
if (!Number.isFinite(this.maxInboundDeadlineMs) || this.maxInboundDeadlineMs <= 0) {
|
|
517
575
|
throw new RangeError("maxInboundDeadlineMs must be a finite positive number");
|
|
518
576
|
}
|
|
519
|
-
this.gracefulSignals =
|
|
577
|
+
this.gracefulSignals =
|
|
578
|
+
options.gracefulSignals === true ? {} : options.gracefulSignals || undefined;
|
|
520
579
|
this.stopHooks = [];
|
|
521
580
|
this.fetchMounts = [];
|
|
522
581
|
this.mountOrder = 0;
|
|
@@ -565,6 +624,7 @@ export class Server {
|
|
|
565
624
|
this.responseBodyTag = undefined;
|
|
566
625
|
this.responseBodyOwners = new Set();
|
|
567
626
|
this.responseRequests = new WeakMap();
|
|
627
|
+
this.wsHookedPasses = undefined;
|
|
568
628
|
this.responseSources = new WeakMap();
|
|
569
629
|
this.nodeContexts = new WeakMap();
|
|
570
630
|
this.appliedPlugins = new Map();
|
|
@@ -659,6 +719,15 @@ export class Server {
|
|
|
659
719
|
opaque: options.opaque,
|
|
660
720
|
app,
|
|
661
721
|
};
|
|
722
|
+
// Mounts run before routes: a request under this prefix that no mount on the way strips comes back
|
|
723
|
+
// here unchanged until the stack overflows, which takes the process down. Dev-time only, like the
|
|
724
|
+
// hook audit: a production define folds this away, and the first dev request would hit it anyway.
|
|
725
|
+
if (hookAuditRuntime &&
|
|
726
|
+
process.env.NODE_ENV !== "production" &&
|
|
727
|
+
!mount.stripPrefix &&
|
|
728
|
+
mountLeadsBack(app, this, mount.path, (server) => server.fetchMounts, new Set())) {
|
|
729
|
+
throw new RouteConfigError("MOUNT_CYCLE", `mount ${mount.path}: the app leads back to this one without stripPrefix`);
|
|
730
|
+
}
|
|
662
731
|
this.fetchMounts.push(mount);
|
|
663
732
|
this.fetchMounts.sort(compareMounts);
|
|
664
733
|
this.preRouteMountCount += 1;
|
|
@@ -679,8 +748,10 @@ export class Server {
|
|
|
679
748
|
derive(fn) {
|
|
680
749
|
this.assertConfigurable("derive()");
|
|
681
750
|
this.derives.push(fn);
|
|
682
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
683
|
-
recordScopedHook(this, this.catalog.size, "derive");
|
|
751
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
752
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "derive");
|
|
753
|
+
tagHook(fn, "derive", this);
|
|
754
|
+
}
|
|
684
755
|
return this;
|
|
685
756
|
}
|
|
686
757
|
/**
|
|
@@ -710,22 +781,27 @@ export class Server {
|
|
|
710
781
|
throw new TypeError('authenticate() stage mode must be "sync" or "async"');
|
|
711
782
|
}
|
|
712
783
|
this.authStages.push(stage);
|
|
784
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
785
|
+
tagHook(stage, "authenticate", this);
|
|
713
786
|
return this;
|
|
714
787
|
}
|
|
715
788
|
decorate(key, value) {
|
|
716
789
|
this.assertConfigurable("decorate()");
|
|
717
790
|
this.decorations[key] = value;
|
|
718
791
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
719
|
-
recordScopedHook(this, this.catalog.size, "decorate");
|
|
792
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "decorate");
|
|
720
793
|
return this;
|
|
721
794
|
}
|
|
722
795
|
/**
|
|
723
|
-
* Run before routing on the raw request. Return a `Response`
|
|
724
|
-
* `Request` to continue routing with a rewritten method/URL/headers.
|
|
796
|
+
* Run before routing on the raw request. Return a `Response` or a `status()` result to
|
|
797
|
+
* short-circuit, or a replacement `Request` to continue routing with a rewritten method/URL/headers.
|
|
798
|
+
* Global.
|
|
725
799
|
*/
|
|
726
800
|
onRequest(fn) {
|
|
727
801
|
this.assertConfigurable("onRequest()");
|
|
728
802
|
this.onRequestHooks.push(fn);
|
|
803
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
804
|
+
tagHook(fn, "onRequest", this);
|
|
729
805
|
this.onNodeRequestHooks.push(undefined);
|
|
730
806
|
this.nodeRequestHooksComplete = false;
|
|
731
807
|
this.bunNativeRequestHooksSafe = false;
|
|
@@ -739,8 +815,10 @@ export class Server {
|
|
|
739
815
|
beforeHandle(fn) {
|
|
740
816
|
this.assertConfigurable("beforeHandle()");
|
|
741
817
|
this.beforeHandleHooks.push(fn);
|
|
742
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
743
|
-
recordScopedHook(this, this.catalog.size, "beforeHandle");
|
|
818
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
819
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "beforeHandle");
|
|
820
|
+
tagHook(fn, "beforeHandle", this);
|
|
821
|
+
}
|
|
744
822
|
return this;
|
|
745
823
|
}
|
|
746
824
|
/**
|
|
@@ -754,8 +832,10 @@ export class Server {
|
|
|
754
832
|
around(fn) {
|
|
755
833
|
this.assertConfigurable("around()");
|
|
756
834
|
this.aroundHooks.push(fn);
|
|
757
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
835
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
758
836
|
recordScopedHook(this, this.catalog.size, "around");
|
|
837
|
+
tagHook(fn, "around", this);
|
|
838
|
+
}
|
|
759
839
|
return this;
|
|
760
840
|
}
|
|
761
841
|
/**
|
|
@@ -793,8 +873,10 @@ export class Server {
|
|
|
793
873
|
afterHandle(fn) {
|
|
794
874
|
this.assertConfigurable("afterHandle()");
|
|
795
875
|
this.afterHandleHooks.push(fn);
|
|
796
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
876
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
797
877
|
recordScopedHook(this, this.catalog.size, "afterHandle");
|
|
878
|
+
tagHook(fn, "afterHandle", this);
|
|
879
|
+
}
|
|
798
880
|
return this;
|
|
799
881
|
}
|
|
800
882
|
/** Handle a thrown error; a non-`undefined` return becomes the response (else the default 500).
|
|
@@ -803,14 +885,18 @@ export class Server {
|
|
|
803
885
|
onError(fn) {
|
|
804
886
|
this.assertConfigurable("onError()");
|
|
805
887
|
this.onErrorHooks.push(fn);
|
|
806
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
807
|
-
recordScopedHook(this, this.catalog.size, "onError");
|
|
888
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
889
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "onError");
|
|
890
|
+
tagHook(fn, "onError", this);
|
|
891
|
+
}
|
|
808
892
|
return this;
|
|
809
893
|
}
|
|
810
894
|
/** Transform every outgoing response - success, error, 404, 405, short-circuit. Global. */
|
|
811
895
|
onResponse(fn) {
|
|
812
896
|
this.assertConfigurable("onResponse()");
|
|
813
897
|
this.onResponseHooks.push(fn);
|
|
898
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
899
|
+
tagHook(fn, "onResponse", this);
|
|
814
900
|
this.onNodeResponseHooks.push(undefined);
|
|
815
901
|
this.nodeResponseHooksComplete = false;
|
|
816
902
|
this.bunNativeResponseHeadersOnly = false;
|
|
@@ -842,6 +928,10 @@ export class Server {
|
|
|
842
928
|
*/
|
|
843
929
|
responseHeaders(record) {
|
|
844
930
|
this.assertConfigurable("responseHeaders()");
|
|
931
|
+
if (hookAuditRuntime &&
|
|
932
|
+
process.env.NODE_ENV !== "production" &&
|
|
933
|
+
this.onResponseHooks.length === 0)
|
|
934
|
+
tagStatic(this, Object.keys(record), record);
|
|
845
935
|
return this.addStaticResponseHeaders(normalizeStaticResponseHeaders(record));
|
|
846
936
|
}
|
|
847
937
|
/** Wire an already-validated lowercase record into the static tier, or - once a dynamic response
|
|
@@ -858,13 +948,15 @@ export class Server {
|
|
|
858
948
|
});
|
|
859
949
|
return this;
|
|
860
950
|
}
|
|
861
|
-
// Route the hook through the same seam the static tier uses. On a framework response
|
|
862
|
-
//
|
|
863
|
-
//
|
|
864
|
-
//
|
|
865
|
-
//
|
|
951
|
+
// Route the hook through the same seam the static tier uses. On a framework response with a
|
|
952
|
+
// body it applies the defaults in place and returns the SAME object, so the serialized-body
|
|
953
|
+
// marker survives for later `onResponseBody`/`onResponseRaw` observers and the Node writer;
|
|
954
|
+
// reconstructing a `Response` here would strip that marker and reclassify the Node outcome from
|
|
955
|
+
// `json` to a generic `response`. A body-less or guarded response is copied, marker included.
|
|
866
956
|
const statics = buildStaticResponseHeaders(record, this.inheritedStatics);
|
|
867
|
-
this.onResponseHooks.push((response) => applyStaticResponseHeaders(response, statics));
|
|
957
|
+
this.onResponseHooks.push((response, req) => applyStaticResponseHeaders(response, statics, req));
|
|
958
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
959
|
+
tagHook(this.onResponseHooks.at(-1), "responseHeaders", this);
|
|
868
960
|
this.onNodeResponseHooks.push(undefined);
|
|
869
961
|
this.nodeResponseHooksComplete = false;
|
|
870
962
|
return this;
|
|
@@ -898,6 +990,8 @@ export class Server {
|
|
|
898
990
|
onResponseFinalized(fn) {
|
|
899
991
|
this.assertConfigurable("onResponseFinalized()");
|
|
900
992
|
this.onResponseFinalizedHooks.push(fn);
|
|
993
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
994
|
+
tagHook(fn, "onResponseFinalized", this);
|
|
901
995
|
return this;
|
|
902
996
|
}
|
|
903
997
|
use(input) {
|
|
@@ -925,10 +1019,14 @@ export class Server {
|
|
|
925
1019
|
this.activeAssurance.push(...pluginOnly);
|
|
926
1020
|
const inherited = this.inheritedPlugins;
|
|
927
1021
|
this.inheritedPlugins = undefined;
|
|
1022
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1023
|
+
enterPlugin(name);
|
|
928
1024
|
try {
|
|
929
1025
|
return arg(this);
|
|
930
1026
|
}
|
|
931
1027
|
finally {
|
|
1028
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1029
|
+
leavePlugin();
|
|
932
1030
|
this.inheritedPlugins = inherited;
|
|
933
1031
|
// Remove only this plugin's temporary evidence. Nested assured plugins may deliberately leave
|
|
934
1032
|
// subsequent evidence active, so truncating the whole array would lose real ordering semantics.
|
|
@@ -957,6 +1055,8 @@ export class Server {
|
|
|
957
1055
|
if (arg.onRequest !== undefined) {
|
|
958
1056
|
this.assertConfigurable("onRequest()");
|
|
959
1057
|
this.onRequestHooks.push(arg.onRequest);
|
|
1058
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1059
|
+
tagHook(arg.onRequest, "onRequest", this);
|
|
960
1060
|
this.onNodeRequestHooks.push(arg.onNodeRequest);
|
|
961
1061
|
if (arg.onNodeRequest === undefined)
|
|
962
1062
|
this.nodeRequestHooksComplete = false;
|
|
@@ -976,6 +1076,8 @@ export class Server {
|
|
|
976
1076
|
if (arg.onResponse !== undefined) {
|
|
977
1077
|
this.assertConfigurable("onResponse()");
|
|
978
1078
|
this.onResponseHooks.push(arg.onResponse);
|
|
1079
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1080
|
+
tagHook(arg.onResponse, "onResponse", this);
|
|
979
1081
|
this.onNodeResponseHooks.push(arg.onNodeResponse);
|
|
980
1082
|
if (arg.onNodeResponse === undefined)
|
|
981
1083
|
this.nodeResponseHooksComplete = false;
|
|
@@ -1014,8 +1116,10 @@ export class Server {
|
|
|
1014
1116
|
return this.route("GET", path, schemaOrHandler, handler);
|
|
1015
1117
|
}
|
|
1016
1118
|
/**
|
|
1017
|
-
* Register a **typed SSE route** -
|
|
1018
|
-
* payloads are contracted by `schema.sse`.
|
|
1119
|
+
* Register a **typed SSE route** - an endpoint streaming `text/event-stream` whose event
|
|
1120
|
+
* payloads are contracted by `schema.sse`. It is a GET route, or a POST route when the schema
|
|
1121
|
+
* declares a `body` (a GET carries none): the body validates as on any POST, and the client sends
|
|
1122
|
+
* it with `.subscribe(onEvent, { body })` - the shape of a chat stream that posts its prompt. The handler receives the validated context plus a
|
|
1019
1123
|
* {@link TypedSSEStream}: `stream.send(event)` is compile-time-checked against the schema and
|
|
1020
1124
|
* JSON-serialized into the SSE `data:` field. The typed client sees the marker and grows a
|
|
1021
1125
|
* `.subscribe(onEvent)` for the route with the same payload type - end-to-end typed streaming.
|
|
@@ -1030,13 +1134,13 @@ export class Server {
|
|
|
1030
1134
|
*
|
|
1031
1135
|
* // client: const off = api.feed.subscribe((post) => console.log(post.title))
|
|
1032
1136
|
*
|
|
1033
|
-
* `init` passes through to the underlying {@link sse} helper (`keepAlive`,
|
|
1034
|
-
* connection closes when the handler resolves, `stream.close()` runs, or the
|
|
1035
|
-
* (`stream.signal`). Query/body schemas validate exactly as on any other route.
|
|
1137
|
+
* `init` passes through to the underlying {@link sse} helper (`keepAlive`, `maxBufferedBytes`,
|
|
1138
|
+
* extra headers). The connection closes when the handler resolves, `stream.close()` runs, or the
|
|
1139
|
+
* client disconnects (`stream.signal`). Query/body schemas validate exactly as on any other route.
|
|
1036
1140
|
*/
|
|
1037
1141
|
sse(path, schema, run, init) {
|
|
1038
1142
|
const handler = (context) => requireSseRuntime(this.sseRuntime).response(context, (stream) => run(context, stream), init);
|
|
1039
|
-
return this.route("GET", path, schema, handler);
|
|
1143
|
+
return this.route(schema.body ? "POST" : "GET", path, schema, handler);
|
|
1040
1144
|
}
|
|
1041
1145
|
post(path, schemaOrHandler, handler) {
|
|
1042
1146
|
return this.route("POST", path, schemaOrHandler, handler);
|
|
@@ -1092,8 +1196,11 @@ export class Server {
|
|
|
1092
1196
|
}
|
|
1093
1197
|
/**
|
|
1094
1198
|
* Register a **WebSocket** route. The connection upgrades on a `GET` to `path` carrying
|
|
1095
|
-
* `Upgrade: websocket
|
|
1096
|
-
*
|
|
1199
|
+
* `Upgrade: websocket`. The handshake runs the chain an HTTP `GET` registered here would: the
|
|
1200
|
+
* `authenticate()` stages, `derive`s and `beforeHandle`s added before this call, then the optional
|
|
1201
|
+
* `handler.upgrade(c)`. Any of them may reject (a `Response`, a `status(...)`, a failed auth stage);
|
|
1202
|
+
* `upgrade()` may seed per-connection `ws.data`. To keep a socket public, register it before the
|
|
1203
|
+
* guard. WebSockets are served by the
|
|
1097
1204
|
* adapter (`listen()`, `@nifrajs/node`, `@nifrajs/deno`, `toFetchHandler`) - not by bare `app.fetch`, which
|
|
1098
1205
|
* has no socket (a WS path through `app.fetch` is a normal HTTP response).
|
|
1099
1206
|
*
|
|
@@ -1120,10 +1227,20 @@ export class Server {
|
|
|
1120
1227
|
this.topics ??= runtime.createTopics();
|
|
1121
1228
|
// A `messageSchema` wraps `message` with validation once, here - every adapter then dispatches
|
|
1122
1229
|
// already-validated, typed messages (Bun/Deno/Node/Workers) with no per-adapter code.
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1230
|
+
const wrapped = runtime.wrapHandler(handler, this.protoPoisoning);
|
|
1231
|
+
const upgrade = runtime.upgradeStep(wrapped);
|
|
1232
|
+
for (const form of expandOptionalParams(path)) {
|
|
1233
|
+
// The chain an HTTP `GET` to `form` would capture now, ending in `upgrade()` instead of a
|
|
1234
|
+
// handler. `afterHandle` and `around` are left out: they wrap a response, and an accepted
|
|
1235
|
+
// handshake has none.
|
|
1236
|
+
const { entry, assurance, descriptor } = this.prepareRoute("WS", form, wrapped.capabilities === undefined ? undefined : { capabilities: wrapped.capabilities }, upgrade);
|
|
1237
|
+
const chain = { ...entry, afterHandle: [] };
|
|
1238
|
+
this.wsRouter.add("GET", form, {
|
|
1239
|
+
handler: wrapped,
|
|
1240
|
+
chain: { ...chain, program: compileRouteProgram({ ...chain, bodySchema: undefined }) },
|
|
1241
|
+
});
|
|
1242
|
+
this.wsRoutes.push({ path: form, capabilities: descriptor.capabilities, assurance, chain });
|
|
1243
|
+
}
|
|
1127
1244
|
wsChanges += 1;
|
|
1128
1245
|
return this;
|
|
1129
1246
|
}
|
|
@@ -1315,11 +1432,6 @@ export class Server {
|
|
|
1315
1432
|
assurance: routeAssurance,
|
|
1316
1433
|
};
|
|
1317
1434
|
}
|
|
1318
|
-
/** The idempotency lane's bridge back into the normal matched lanes, resolved to a concrete Response
|
|
1319
|
-
* (the lane buffers the body, then replays it through the route's real validation + handler). */
|
|
1320
|
-
idempotencyRunLanes(buffered, platform, entry, params, search) {
|
|
1321
|
-
return Promise.resolve(this.runMatchedLanes(buffered, platform, entry, params, search, this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, false));
|
|
1322
|
-
}
|
|
1323
1435
|
/** @internal Symbol-keyed install seam for the `responseContract()` plugin. Off the public typed surface. */
|
|
1324
1436
|
[INSTALL_RESPONSE_CONTRACT](runtime) {
|
|
1325
1437
|
this.assertConfigurable("responseContract()");
|
|
@@ -1332,6 +1444,8 @@ export class Server {
|
|
|
1332
1444
|
assertConfigurable: (operation) => this.assertConfigurable(operation),
|
|
1333
1445
|
addResponseHook: (web, node, headerOnly = false) => {
|
|
1334
1446
|
this.onResponseHooks.push(web);
|
|
1447
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1448
|
+
tagHook(web, headerOnly ? "responseHeaders" : "onResponse", this);
|
|
1335
1449
|
this.onNodeResponseHooks.push(node);
|
|
1336
1450
|
if (node === undefined)
|
|
1337
1451
|
this.nodeResponseHooksComplete = false;
|
|
@@ -1370,6 +1484,11 @@ export class Server {
|
|
|
1370
1484
|
this.assertConfigurable("nodeDirect()");
|
|
1371
1485
|
this.nodeOutcomeRuntime = runtime;
|
|
1372
1486
|
}
|
|
1487
|
+
/** @internal An adapter reports a failure no response hook saw (a hook that threw past `fetch`, a
|
|
1488
|
+
* response the runtime refused to write), logged as Bun's `listen()` logs one. */
|
|
1489
|
+
[REPORT_ERROR](error) {
|
|
1490
|
+
emitRequestErrorLog(this.logger, this.errorLogDetail, error, undefined);
|
|
1491
|
+
}
|
|
1373
1492
|
/** @internal Symbol-keyed install seam for the `streaming()` plugin. Off the public typed surface. */
|
|
1374
1493
|
[INSTALL_SSE](runtime) {
|
|
1375
1494
|
this.assertConfigurable("streaming()");
|
|
@@ -1388,7 +1507,7 @@ export class Server {
|
|
|
1388
1507
|
* counts. `seen` ends a mount cycle (an app mounted under itself to alias a prefix).
|
|
1389
1508
|
*/
|
|
1390
1509
|
[GET_WS_RUNTIME](seen) {
|
|
1391
|
-
return this.
|
|
1510
|
+
return this.wsRoutes.length > 0 ? this.wsRuntime : this.mountedWsRuntime(seen);
|
|
1392
1511
|
}
|
|
1393
1512
|
/** The `[GET_WS_RUNTIME]` answer for the apps mounted here alone, whatever this app's own routes. */
|
|
1394
1513
|
mountedWsRuntime(seen = new Set()) {
|
|
@@ -1429,15 +1548,36 @@ export class Server {
|
|
|
1429
1548
|
* `derive`/`decorate`/`beforeHandle`/`afterHandle`/`onError`/`around` apply to its routes
|
|
1430
1549
|
* exactly as they did standalone, so a group wires its own plugins. The same locality rule
|
|
1431
1550
|
* covers the group's `onRequest` hooks: they run only for requests that route to the GROUP's
|
|
1432
|
-
* routes (a `bodyLimit()` mounted on an uploads group must not start gating the whole app
|
|
1551
|
+
* routes, and for an `OPTIONS` preflight to a path they serve (a `bodyLimit()` mounted on an uploads group must not start gating the whole app
|
|
1433
1552
|
* because the app composed it in), and the group's global assurance follows its hooks onto
|
|
1434
1553
|
* exactly those routes. A group with hooks but NO routes is a middleware bundle - its hooks
|
|
1435
1554
|
* can only mean app-wide intent, so they are appended globally, unchanged. Response-side hooks
|
|
1436
1555
|
* (`onResponse`/`onResponseFinalized`) are appended to this server's. This server's
|
|
1437
1556
|
* route-scoped chains do NOT retroactively wrap merged routes (order-scoped, like routes
|
|
1438
1557
|
* registered before a `derive`). Fail closed: a path+method collision throws
|
|
1439
|
-
* `RouteConfigError` at merge time, and a group with WebSocket routes is refused
|
|
1440
|
-
* those on the parent).
|
|
1558
|
+
* `RouteConfigError` at merge time, and a group with WebSocket routes or mounts is refused
|
|
1559
|
+
* (register those on the parent).
|
|
1560
|
+
*
|
|
1561
|
+
* What this server's `.use()`, made BEFORE the merge, covers on a merged route versus a
|
|
1562
|
+
* `group()` route:
|
|
1563
|
+
*
|
|
1564
|
+
* | installed on this server | group() route | merge()d route |
|
|
1565
|
+
* | ------------------------------------------ | ------------- | ------------------------------ |
|
|
1566
|
+
* | derive/decorate/beforeHandle/afterHandle, | yes | no - wire it on the merged |
|
|
1567
|
+
* | onError/around, authenticate, route-scoped | | server |
|
|
1568
|
+
* | assurance | | |
|
|
1569
|
+
* | onRequest/onResponse/responseHeaders hooks | yes | yes (app-wide) |
|
|
1570
|
+
* | responseContract() | yes | yes, when the route declares a |
|
|
1571
|
+
* | | | response and its own server |
|
|
1572
|
+
* | | | installed no contract |
|
|
1573
|
+
* | effectLedger() | yes | yes, when the route declares |
|
|
1574
|
+
* | | | capabilities and has no ledger |
|
|
1575
|
+
* | idempotency() | yes | the merged server's own; a |
|
|
1576
|
+
* | | | route declaring it without one |
|
|
1577
|
+
* | | | is refused at registration |
|
|
1578
|
+
*
|
|
1579
|
+
* The response contract and the ledger can only add a check or a record, so a merged route is
|
|
1580
|
+
* recompiled under them. A runtime installed AFTER the merge covers neither kind of route.
|
|
1441
1581
|
*/
|
|
1442
1582
|
merge(other) {
|
|
1443
1583
|
this.assertConfigurable("merge()");
|
|
@@ -1452,8 +1592,8 @@ export class Server {
|
|
|
1452
1592
|
throw new RouteConfigError("INVALID_PATH", "merge() inside a group cannot prefix compiled routes - merge on the parent");
|
|
1453
1593
|
}
|
|
1454
1594
|
const source = other;
|
|
1455
|
-
if (source.
|
|
1456
|
-
throw new RouteConfigError("INVALID_PATH", "merge() does not carry WebSocket routes - register
|
|
1595
|
+
if (source.wsRoutes.length > 0 || source.fetchMounts.length > 0) {
|
|
1596
|
+
throw new RouteConfigError("INVALID_PATH", "merge() does not carry WebSocket routes or mounts - register them on the parent server");
|
|
1457
1597
|
}
|
|
1458
1598
|
this.adopt(source, undefined, 0);
|
|
1459
1599
|
return this;
|
|
@@ -1473,7 +1613,7 @@ export class Server {
|
|
|
1473
1613
|
* The builder receives a scope that INHERITS this server's route chain as it stands at the call
|
|
1474
1614
|
* (`derive`/`decorate`/`authenticate`/`beforeHandle`/`afterHandle`/`onError`/`around`, assurance,
|
|
1475
1615
|
* capability interceptors, installed runtimes, applied plugins), exactly as a route declared here
|
|
1476
|
-
* would. A plugin this server already applied is in effect for the group: the group's `use()` of
|
|
1616
|
+
* would. {@link merge} documents how that differs for a merged server. A plugin this server already applied is in effect for the group: the group's `use()` of
|
|
1477
1617
|
* that same instance does nothing, and its `use()` of another instance under the same name throws a
|
|
1478
1618
|
* `RouteConfigError` (`PLUGIN_RECONFIGURED`), because that instance could never replace the parent's.
|
|
1479
1619
|
* A plugin the group applies still shares the parent's copy of a dependency it `use()`s itself.
|
|
@@ -1534,13 +1674,13 @@ export class Server {
|
|
|
1534
1674
|
if (built !== scope) {
|
|
1535
1675
|
throw new TypeError(`${label} builder must return its group synchronously`);
|
|
1536
1676
|
}
|
|
1537
|
-
if (scope.
|
|
1677
|
+
if (scope.wsRoutes.length > 0 ||
|
|
1538
1678
|
scope.fetchMounts.length > 0 ||
|
|
1539
1679
|
scope.catalog.entries().some((route) => route.descriptor.tool !== undefined)) {
|
|
1540
1680
|
throw new RouteConfigError("INVALID_PATH", `${label} cannot hold ws(), mount, or MCP tool routes - add them to the parent`);
|
|
1541
1681
|
}
|
|
1542
1682
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1543
|
-
sealHookAudit(scope, scope.catalog.size, this.logger);
|
|
1683
|
+
sealHookAudit(scope, scope.catalog.size, scope.wsRoutes.length, this.logger);
|
|
1544
1684
|
this.adopt(scope, fullPrefix, this.globalAssurance.length);
|
|
1545
1685
|
return this;
|
|
1546
1686
|
}
|
|
@@ -1567,7 +1707,7 @@ export class Server {
|
|
|
1567
1707
|
assurance: [...route.assurance, ...ownGlobal],
|
|
1568
1708
|
})
|
|
1569
1709
|
: (route) => route;
|
|
1570
|
-
this.catalog.addBatch(sourceRoutes.map((route) => this.bindFusedRuntime(foldedAssurance(route))));
|
|
1710
|
+
this.catalog.addBatch(sourceRoutes.map((route) => this.bindFusedRuntime(foldedAssurance(adoptRouteRuntimes(route, this.responseContractRuntime, this.effectLedgerRuntime)))));
|
|
1571
1711
|
// Resolved idempotency/ledger route entries carry their own store/sink configuration, while the
|
|
1572
1712
|
// runtime object supplies the generic execution machinery. If the parent already has a runtime,
|
|
1573
1713
|
// either implementation can execute every resolved entry because route-specific options were
|
|
@@ -1594,13 +1734,17 @@ export class Server {
|
|
|
1594
1734
|
// The group's static declarations came before its own response hooks, so they are folded in
|
|
1595
1735
|
// first - and fold themselves into a hook here if this server already has one (same ordering
|
|
1596
1736
|
// rule as a direct `responseHeaders()` call).
|
|
1737
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1738
|
+
adoptStaticTags(this, source, this.onResponseHooks.length > 0);
|
|
1597
1739
|
this.addStaticResponseHeaders({ ...statics.record });
|
|
1598
1740
|
}
|
|
1599
1741
|
else {
|
|
1600
1742
|
// The static tier is folded into EVERY response this server builds, so a group's static
|
|
1601
1743
|
// headers cannot join it. A hook ahead of the group's own (the sealed scope is discarded
|
|
1602
1744
|
// after adoption) keeps their meaning - defaults a value already on the response wins over.
|
|
1603
|
-
source.onResponseHooks.unshift((response) => applyStaticResponseHeaders(response, statics));
|
|
1745
|
+
source.onResponseHooks.unshift((response, req) => applyStaticResponseHeaders(response, statics, req));
|
|
1746
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1747
|
+
tagHook(source.onResponseHooks[0], "responseHeaders", source, true);
|
|
1604
1748
|
source.onNodeResponseHooks.unshift(undefined);
|
|
1605
1749
|
source.nodeResponseHooksComplete = false;
|
|
1606
1750
|
}
|
|
@@ -1608,6 +1752,17 @@ export class Server {
|
|
|
1608
1752
|
this.onResponseHooks.push(...scopeHooks(source.onResponseHooks, responseScope, 1, true));
|
|
1609
1753
|
this.onNodeResponseHooks.push(...scopeHooks(source.onNodeResponseHooks, responseScope, 1));
|
|
1610
1754
|
this.onResponseFinalizedHooks.push(...scopeHooks(source.onResponseFinalizedHooks, responseScope, 1));
|
|
1755
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1756
|
+
// Read before anything else is appended: the wrappers are still each list's tail.
|
|
1757
|
+
const pairs = [];
|
|
1758
|
+
if (inScope !== undefined)
|
|
1759
|
+
pairs.push([source.onRequestHooks, this.onRequestHooks]);
|
|
1760
|
+
if (responseScope !== undefined) {
|
|
1761
|
+
pairs.push([source.onResponseHooks, this.onResponseHooks]);
|
|
1762
|
+
pairs.push([source.onResponseFinalizedHooks, this.onResponseFinalizedHooks]);
|
|
1763
|
+
}
|
|
1764
|
+
tagAdopted(this, source, prefix, sourceRoutes, pairs);
|
|
1765
|
+
}
|
|
1611
1766
|
this.nodeResponseHooksComplete &&= source.nodeResponseHooksComplete;
|
|
1612
1767
|
this.bunNativeResponseHeadersOnly &&= source.bunNativeResponseHeadersOnly;
|
|
1613
1768
|
this.hasRawNodeResponseHook ||= source.hasRawNodeResponseHook;
|
|
@@ -1724,7 +1879,7 @@ export class Server {
|
|
|
1724
1879
|
// Seal once on the first request so test servers (no `listen()`) still get the dead-hook check.
|
|
1725
1880
|
// After that it is a single boolean read - the per-request cost the option promises to keep at zero.
|
|
1726
1881
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1727
|
-
sealHookAudit(this, this.catalog.size, this.logger);
|
|
1882
|
+
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger);
|
|
1728
1883
|
// Off path (default): straight through - one property check, no closure, no promise.
|
|
1729
1884
|
if (this.capacityGate === undefined)
|
|
1730
1885
|
return this.fetchSourceInner(source, platform);
|
|
@@ -1857,7 +2012,7 @@ export class Server {
|
|
|
1857
2012
|
resolveNodeSource(source, platform, suppliedRuntime) {
|
|
1858
2013
|
// Seal once on the Node-direct lane too - it can bypass `fetchSource` entirely (hookless outcome).
|
|
1859
2014
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1860
|
-
sealHookAudit(this, this.catalog.size, this.logger);
|
|
2015
|
+
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger);
|
|
1861
2016
|
// A paired header-only native hook can preserve the Node-direct JSON/body outcome for successful
|
|
1862
2017
|
// responses. Arbitrary onResponse transforms need a real Web Response, but a buffered outcome can
|
|
1863
2018
|
// be materialized with a direct-write marker and return to the socket path when the hook mutates
|
|
@@ -2075,7 +2230,7 @@ export class Server {
|
|
|
2075
2230
|
continue;
|
|
2076
2231
|
}
|
|
2077
2232
|
if (outcome !== undefined)
|
|
2078
|
-
return outcome;
|
|
2233
|
+
return webResponseOf(outcome);
|
|
2079
2234
|
}
|
|
2080
2235
|
return current;
|
|
2081
2236
|
}
|
|
@@ -2086,7 +2241,7 @@ export class Server {
|
|
|
2086
2241
|
if (outcome instanceof Request)
|
|
2087
2242
|
next = outcome;
|
|
2088
2243
|
else if (outcome !== undefined)
|
|
2089
|
-
return outcome;
|
|
2244
|
+
return webResponseOf(outcome);
|
|
2090
2245
|
if (i >= this.onRequestHooks.length)
|
|
2091
2246
|
return next;
|
|
2092
2247
|
const hook = this.onRequestHooks[i];
|
|
@@ -2098,7 +2253,7 @@ export class Server {
|
|
|
2098
2253
|
resolveWebSocketUpgrade(req, platform) {
|
|
2099
2254
|
// Before any header is read: Deno only materializes a request's headers when asked, and that
|
|
2100
2255
|
// costs a plain request measurably.
|
|
2101
|
-
if (this.
|
|
2256
|
+
if (this.wsRoutes.length === 0 && !this.mountsTakeUpgrades())
|
|
2102
2257
|
return WS_PASS;
|
|
2103
2258
|
if (req.headers.get("upgrade")?.toLowerCase() !== "websocket")
|
|
2104
2259
|
return WS_PASS;
|
|
@@ -2106,6 +2261,8 @@ export class Server {
|
|
|
2106
2261
|
// is an ordinary request: its HTTP route answers it, and normal routing refuses an unknown token.
|
|
2107
2262
|
if (req.method !== "GET")
|
|
2108
2263
|
return WS_PASS;
|
|
2264
|
+
// The adapter answers a pass by handing this same object to `fetch`.
|
|
2265
|
+
const received = req;
|
|
2109
2266
|
// The handshake routes the path `fetch` would: dot segments resolved (see `fetch`).
|
|
2110
2267
|
if (rawRequestTargets && hasDotSegment(req.url))
|
|
2111
2268
|
req = withResolvedTarget(req);
|
|
@@ -2119,11 +2276,12 @@ export class Server {
|
|
|
2119
2276
|
const controller = timeoutMs > 0 ? new AbortController() : undefined;
|
|
2120
2277
|
const signal = controller?.signal ?? getNeverAbortSignal();
|
|
2121
2278
|
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : undefined;
|
|
2122
|
-
|
|
2123
|
-
const
|
|
2279
|
+
// A refusal carries the static response headers an HTTP answer would; a guard's already does.
|
|
2280
|
+
const refuse = (response) => ({
|
|
2124
2281
|
kind: "reject",
|
|
2125
|
-
response:
|
|
2282
|
+
response: this.wrapWebResponse(response),
|
|
2126
2283
|
});
|
|
2284
|
+
const onTimeout = () => refuse(jsonError(503, "request_timeout"));
|
|
2127
2285
|
const bounded = (work, onDone) => controller === undefined
|
|
2128
2286
|
? work
|
|
2129
2287
|
: this.withTimeout(work, controller, onDone, Math.max(0, deadline - Date.now()));
|
|
@@ -2136,11 +2294,11 @@ export class Server {
|
|
|
2136
2294
|
// already did and made every plain dynamic route pay for unrelated static path bytes.
|
|
2137
2295
|
const params = match.params === EMPTY_PARAMS ? match.params : decodeRouteParams(match.params);
|
|
2138
2296
|
if (params === null)
|
|
2139
|
-
return
|
|
2140
|
-
const handler = match.payload
|
|
2141
|
-
// Non-null:
|
|
2297
|
+
return refuse(jsonError(400, "malformed_path"));
|
|
2298
|
+
const { handler, chain } = match.payload;
|
|
2299
|
+
// Non-null: a socket route ⇒ ws() ran ⇒ `.use(websocket())` installed the runtime + registry.
|
|
2142
2300
|
const pubsub = this.topics;
|
|
2143
|
-
const
|
|
2301
|
+
const runtime = this.wsRuntime;
|
|
2144
2302
|
// CSWSH guard, before any per-connection work or the user's upgrade(): reject a disallowed
|
|
2145
2303
|
// Origin with 403. Browsers don't CORS-protect WS handshakes but do send cookies, so this
|
|
2146
2304
|
// blocks cross-site authenticated sockets when the route opts in via `allowedOrigins`.
|
|
@@ -2150,52 +2308,46 @@ export class Server {
|
|
|
2150
2308
|
? handler.allowedOrigins(origin)
|
|
2151
2309
|
: origin !== null && handler.allowedOrigins.includes(origin);
|
|
2152
2310
|
if (!allowed)
|
|
2153
|
-
return
|
|
2311
|
+
return refuse(jsonError(403, "forbidden_origin"));
|
|
2154
2312
|
}
|
|
2155
2313
|
else if (origin !== null && !wsSameOrigin(origin, request)) {
|
|
2156
2314
|
// Secure default (no explicit `allowedOrigins`): reject a CROSS-ORIGIN browser handshake - the
|
|
2157
2315
|
// CSWSH case, since browsers send cookies on WS handshakes and don't apply CORS. Non-browser
|
|
2158
2316
|
// clients send no `Origin` and pass; same-origin browsers pass. Set `allowedOrigins` to permit
|
|
2159
2317
|
// specific cross-origin clients (or `() => true` for a genuinely public socket).
|
|
2160
|
-
return
|
|
2161
|
-
}
|
|
2162
|
-
if (handler.upgrade === undefined) {
|
|
2163
|
-
return {
|
|
2164
|
-
kind: "upgrade",
|
|
2165
|
-
handler,
|
|
2166
|
-
data: undefined,
|
|
2167
|
-
pubsub,
|
|
2168
|
-
attach,
|
|
2169
|
-
maxPayloadBytes: this.wsMaxPayloadBytes,
|
|
2170
|
-
};
|
|
2318
|
+
return refuse(jsonError(403, "forbidden_origin"));
|
|
2171
2319
|
}
|
|
2172
2320
|
const budget = controller === undefined
|
|
2173
2321
|
? createUnboundedRequestBudget(signal)
|
|
2174
2322
|
: createRequestBudget({ deadline: deadline, signal });
|
|
2175
2323
|
const ctx = new RequestContext(request, params, url.search, signal, budget, platform, this.maxBodyBytes, this.protoPoisoning);
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2324
|
+
// Rendered as the same chain on an HTTP `GET` renders its early results.
|
|
2325
|
+
const finalize = (result, set) => {
|
|
2326
|
+
const accepted = runtime.accepted(result);
|
|
2327
|
+
return accepted === undefined
|
|
2328
|
+
? { kind: "reject", response: this.finalizeResponse(result, set) }
|
|
2329
|
+
: {
|
|
2330
|
+
kind: "upgrade",
|
|
2331
|
+
handler,
|
|
2332
|
+
data: accepted.data,
|
|
2333
|
+
pubsub,
|
|
2334
|
+
attach: runtime.attach,
|
|
2335
|
+
maxPayloadBytes: this.wsMaxPayloadBytes,
|
|
2336
|
+
sender: runtime.sender,
|
|
2337
|
+
};
|
|
2338
|
+
};
|
|
2339
|
+
const wrap = (response) => finalize(response, EMPTY_RESPONSE_CONTROLS);
|
|
2340
|
+
let outcome;
|
|
2186
2341
|
try {
|
|
2187
|
-
|
|
2188
|
-
if (!(result instanceof Promise))
|
|
2189
|
-
return settle(result);
|
|
2190
|
-
const pending = result.then(settle, () => ({
|
|
2191
|
-
kind: "reject",
|
|
2192
|
-
response: jsonError(500, "internal_error"),
|
|
2193
|
-
}));
|
|
2194
|
-
return bounded(pending, onTimeout);
|
|
2342
|
+
outcome = executeRouteProgram(this, chain, chain.program, request, ctx, finalize, wrap);
|
|
2195
2343
|
}
|
|
2196
2344
|
catch {
|
|
2197
|
-
return
|
|
2345
|
+
return refuse(jsonError(500, "internal_error"));
|
|
2198
2346
|
}
|
|
2347
|
+
if (!(outcome instanceof Promise))
|
|
2348
|
+
return outcome;
|
|
2349
|
+
const pending = outcome.then(undefined, () => refuse(jsonError(500, "internal_error")));
|
|
2350
|
+
return bounded(pending, onTimeout);
|
|
2199
2351
|
};
|
|
2200
2352
|
const resolveMounted = (request) => {
|
|
2201
2353
|
// Native pub/sub was chosen because no mounted app took upgrades when `listen()` ran. One that
|
|
@@ -2217,20 +2369,25 @@ export class Server {
|
|
|
2217
2369
|
}
|
|
2218
2370
|
if (selected === undefined)
|
|
2219
2371
|
return WS_PASS;
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2372
|
+
let childRequest = request;
|
|
2373
|
+
if (selected.stripPrefix) {
|
|
2374
|
+
childRequest = stripMountPrefix(request, selected.path);
|
|
2375
|
+
selected.wsPassed ??= new WeakMap();
|
|
2376
|
+
selected.wsPassed.set(request, childRequest);
|
|
2377
|
+
}
|
|
2223
2378
|
let child;
|
|
2224
2379
|
try {
|
|
2225
2380
|
child = selected.resolveWebSocketUpgrade(childRequest, platform);
|
|
2226
2381
|
}
|
|
2227
2382
|
catch {
|
|
2228
|
-
return
|
|
2383
|
+
return refuse(jsonError(500, "internal_error"));
|
|
2229
2384
|
}
|
|
2230
2385
|
const settle = (outcome) => outcome.kind === "pass" ||
|
|
2231
2386
|
(outcome.kind === "reject" && outcome.response.status === 404 && selected.fallbackOn404)
|
|
2232
2387
|
? dispatch(selectedIndex + 1)
|
|
2233
|
-
: outcome
|
|
2388
|
+
: outcome.kind === "reject"
|
|
2389
|
+
? refuse(outcome.response)
|
|
2390
|
+
: outcome;
|
|
2234
2391
|
return child instanceof Promise ? child.then(settle) : settle(child);
|
|
2235
2392
|
};
|
|
2236
2393
|
return dispatch(0);
|
|
@@ -2248,21 +2405,24 @@ export class Server {
|
|
|
2248
2405
|
};
|
|
2249
2406
|
if (this.onRequestHooks.length === 0)
|
|
2250
2407
|
return resolveWithDeadline(req);
|
|
2408
|
+
// On a pass the adapter hands `received` to `fetch`, which then routes these results. An upgrade
|
|
2409
|
+
// or a rejection never reaches `fetch`, so the entry dies with the request.
|
|
2410
|
+
const resolveHooked = (request) => {
|
|
2411
|
+
this.wsHookedPasses ??= new WeakMap();
|
|
2412
|
+
this.wsHookedPasses.set(received, request);
|
|
2413
|
+
return resolveWithDeadline(request);
|
|
2414
|
+
};
|
|
2251
2415
|
let hooked;
|
|
2252
2416
|
try {
|
|
2253
2417
|
hooked = this.runWebSocketRequestHooks(req, platform);
|
|
2254
2418
|
}
|
|
2255
2419
|
catch {
|
|
2256
|
-
return
|
|
2420
|
+
return refuse(jsonError(500, "internal_error"));
|
|
2257
2421
|
}
|
|
2258
2422
|
if (!(hooked instanceof Promise)) {
|
|
2259
|
-
return hooked instanceof Response
|
|
2260
|
-
? { kind: "reject", response: hooked }
|
|
2261
|
-
: resolveWithDeadline(hooked);
|
|
2423
|
+
return hooked instanceof Response ? refuse(hooked) : resolveHooked(hooked);
|
|
2262
2424
|
}
|
|
2263
|
-
const pending = hooked.then((result) => result instanceof Response
|
|
2264
|
-
? { kind: "reject", response: result }
|
|
2265
|
-
: resolveWithDeadline(result), () => ({ kind: "reject", response: jsonError(500, "internal_error") }));
|
|
2425
|
+
const pending = hooked.then((result) => (result instanceof Response ? refuse(result) : resolveHooked(result)), () => refuse(jsonError(500, "internal_error")));
|
|
2266
2426
|
return bounded(Promise.resolve(pending).then((result) => result), onTimeout);
|
|
2267
2427
|
}
|
|
2268
2428
|
/** Bun `fetch` when WS routes exist: try a WS upgrade first, else run the normal HTTP lifecycle.
|
|
@@ -2274,9 +2434,8 @@ export class Server {
|
|
|
2274
2434
|
return this.fetch(req, bunPeerPlatform(server, req));
|
|
2275
2435
|
if (o.kind === "reject")
|
|
2276
2436
|
return o.response;
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
})
|
|
2437
|
+
// A copy: the Bun socket keeps per-connection state on it, and a hand-built outcome may be shared.
|
|
2438
|
+
return server.upgrade(req, { data: { ...o } })
|
|
2280
2439
|
? undefined
|
|
2281
2440
|
: jsonError(426, "upgrade_required");
|
|
2282
2441
|
};
|
|
@@ -2319,6 +2478,13 @@ export class Server {
|
|
|
2319
2478
|
if (this.onRequestHooks.length === 0 || !isRoutableMethod(source.method)) {
|
|
2320
2479
|
return this.routeAndRun(source, resolved, finalize, wrapResponse, onTimeout, webFast);
|
|
2321
2480
|
}
|
|
2481
|
+
const hooked = this.wsHookedPasses?.get(source);
|
|
2482
|
+
if (hooked !== undefined) {
|
|
2483
|
+
const request = requestOf(source);
|
|
2484
|
+
this.wsHookedPasses?.delete(request);
|
|
2485
|
+
this.responseRequests.set(request, hooked);
|
|
2486
|
+
return this.routeAndRun(hooked, resolved, finalize, wrapResponse, onTimeout, webFast);
|
|
2487
|
+
}
|
|
2322
2488
|
if (!webFast && this.canUseNodeRequestHooks()) {
|
|
2323
2489
|
return this.runWithNodeRequest(source, resolved, finalize, wrapResponse, onTimeout);
|
|
2324
2490
|
}
|
|
@@ -2595,7 +2761,9 @@ export class Server {
|
|
|
2595
2761
|
}
|
|
2596
2762
|
if (mount === undefined)
|
|
2597
2763
|
return fallback ?? jsonError(404, "not_found");
|
|
2598
|
-
const childRequest = mount.stripPrefix
|
|
2764
|
+
const childRequest = mount.stripPrefix
|
|
2765
|
+
? (mount.wsPassed?.get(request) ?? stripMountPrefix(request, mount.path))
|
|
2766
|
+
: request;
|
|
2599
2767
|
const outcome = mount.handler(childRequest, platform);
|
|
2600
2768
|
if (!replayable || !mount.fallbackOn404)
|
|
2601
2769
|
return outcome;
|
|
@@ -2631,7 +2799,7 @@ export class Server {
|
|
|
2631
2799
|
if (entry.idempotent !== undefined && this.idempotencyRuntime !== undefined) {
|
|
2632
2800
|
return this.idempotencyRuntime.run(entry.idempotent, requestOf(source), platform, entry, params, search, wrapResponse, {
|
|
2633
2801
|
maxBodyBytes: entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2634
|
-
runLanes: (buffered, plat, ent, prm, srch) => this.
|
|
2802
|
+
runLanes: (buffered, plat, ent, prm, srch) => this.runMatchedLanes(buffered, plat, ent, prm, srch, this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, false),
|
|
2635
2803
|
});
|
|
2636
2804
|
}
|
|
2637
2805
|
return this.runMatchedLanes(source, platform, entry, params, search, finalize, wrapResponse, onTimeout, webFast);
|
|
@@ -2672,11 +2840,20 @@ export class Server {
|
|
|
2672
2840
|
: createRequestBudget({ deadline: admission.deadline, signal });
|
|
2673
2841
|
const plan = entry.execution;
|
|
2674
2842
|
const nativeContext = controller === undefined;
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2843
|
+
let outcome;
|
|
2844
|
+
try {
|
|
2845
|
+
outcome =
|
|
2846
|
+
webFast && plan.fusedWeb !== undefined
|
|
2847
|
+
? plan.fusedWeb(source, params, search, signal, budget, platform, nativeContext)
|
|
2848
|
+
: !webFast && plan.fusedBody !== undefined
|
|
2849
|
+
? plan.fusedBody(source, params, search, signal, budget, platform, false, finalize, wrapResponse)
|
|
2850
|
+
: plan.run(this, entry, source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse);
|
|
2851
|
+
}
|
|
2852
|
+
catch (err) {
|
|
2853
|
+
// A lane's synchronous render throw (a BigInt, a header value with a line break): the lanes
|
|
2854
|
+
// guard only their async continuations, since a `try` around a fused render slows it.
|
|
2855
|
+
return this.contextlessBareError(err, source, params, search, signal, budget, platform, wrapResponse);
|
|
2856
|
+
}
|
|
2680
2857
|
// The request timeout only bounds work that is actually pending - a synchronous (bare) result is
|
|
2681
2858
|
// already complete and can't time out, so it's returned as-is (no 503 race, no promise).
|
|
2682
2859
|
if (controller !== undefined && outcome instanceof Promise) {
|
|
@@ -2704,7 +2881,14 @@ export class Server {
|
|
|
2704
2881
|
return this.contextlessBareError(err, source, params, search, signal, budget, platform, wrapResponse);
|
|
2705
2882
|
}
|
|
2706
2883
|
if (result instanceof Promise) {
|
|
2707
|
-
return result.then((value) =>
|
|
2884
|
+
return result.then((value) => {
|
|
2885
|
+
try {
|
|
2886
|
+
return finalize(value, EMPTY_RESPONSE_CONTROLS);
|
|
2887
|
+
}
|
|
2888
|
+
catch (err) {
|
|
2889
|
+
return this.contextlessBareError(err, source, params, search, signal, budget, platform, wrapResponse);
|
|
2890
|
+
}
|
|
2891
|
+
}, (err) => this.contextlessBareError(err, source, params, search, signal, budget, platform, wrapResponse));
|
|
2708
2892
|
}
|
|
2709
2893
|
return finalize(result, EMPTY_RESPONSE_CONTROLS);
|
|
2710
2894
|
}
|
|
@@ -2740,9 +2924,17 @@ export class Server {
|
|
|
2740
2924
|
}
|
|
2741
2925
|
if (result instanceof Promise) {
|
|
2742
2926
|
// Async handler on an otherwise-bare route: finish on a microtask, with the same error handling.
|
|
2743
|
-
return result.then((value) =>
|
|
2927
|
+
return result.then((value) => {
|
|
2928
|
+
// A synchronous finalize throw reaches runMatchedLanes; this one would reject.
|
|
2929
|
+
try {
|
|
2930
|
+
return finalize(value, responseSet(ctx), ctx);
|
|
2931
|
+
}
|
|
2932
|
+
catch (err) {
|
|
2933
|
+
return this.bareError(err, ctx, finalize, wrapResponse);
|
|
2934
|
+
}
|
|
2935
|
+
}, (err) => this.bareError(err, ctx, finalize, wrapResponse));
|
|
2744
2936
|
}
|
|
2745
|
-
return finalize(result, responseSet(ctx));
|
|
2937
|
+
return finalize(result, responseSet(ctx), ctx);
|
|
2746
2938
|
}
|
|
2747
2939
|
/** Bare-route error rendering - identical to the compiled route program's catch minus the (absent) onError
|
|
2748
2940
|
* loop: a thrown `Response` is returned as deliberate control flow; anything else is logged + 500. */
|
|
@@ -2762,22 +2954,29 @@ export class Server {
|
|
|
2762
2954
|
// `() => ...` can't observe the context - skip allocating one entirely (errors still build
|
|
2763
2955
|
// one for the structured log, exactly like runContextlessBare).
|
|
2764
2956
|
const contextlessHandler = handler;
|
|
2957
|
+
const fail = (err, source, params, search, signal, budget, platform, nativeContext) => logError(err, nativeContext
|
|
2958
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
2959
|
+
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning));
|
|
2765
2960
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
2766
2961
|
let result;
|
|
2767
2962
|
try {
|
|
2768
2963
|
result = contextlessHandler();
|
|
2769
2964
|
}
|
|
2770
2965
|
catch (err) {
|
|
2771
|
-
return
|
|
2772
|
-
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
2773
|
-
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning));
|
|
2966
|
+
return fail(err, source, params, search, signal, budget, platform, nativeContext);
|
|
2774
2967
|
}
|
|
2775
2968
|
if (result instanceof Promise) {
|
|
2776
|
-
return result.then((value) =>
|
|
2777
|
-
|
|
2778
|
-
|
|
2969
|
+
return result.then((value) => {
|
|
2970
|
+
// A synchronous render throw reaches runMatchedLanes; this one would reject.
|
|
2971
|
+
try {
|
|
2972
|
+
return fusedRespondNoSet(value, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, this.onResponseHooks.length === 0 ? undefined : requestOf(source));
|
|
2973
|
+
}
|
|
2974
|
+
catch (err) {
|
|
2975
|
+
return fail(err, source, params, search, signal, budget, platform, nativeContext);
|
|
2976
|
+
}
|
|
2977
|
+
}, (err) => fail(err, source, params, search, signal, budget, platform, nativeContext));
|
|
2779
2978
|
}
|
|
2780
|
-
return fusedRespondNoSet(result, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
|
|
2979
|
+
return fusedRespondNoSet(result, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0, this.onResponseHooks.length === 0 ? undefined : requestOf(source));
|
|
2781
2980
|
};
|
|
2782
2981
|
}
|
|
2783
2982
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
@@ -2794,11 +2993,21 @@ export class Server {
|
|
|
2794
2993
|
return logError(err, ctx);
|
|
2795
2994
|
}
|
|
2796
2995
|
if (result instanceof Promise) {
|
|
2797
|
-
return result.then((value) =>
|
|
2996
|
+
return result.then((value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
|
|
2798
2997
|
}
|
|
2799
2998
|
return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
|
|
2800
2999
|
};
|
|
2801
3000
|
}
|
|
3001
|
+
/** A fused Web lane's render in an async continuation, where a result that cannot be rendered
|
|
3002
|
+
* must fail like a handler throw; a synchronous render throw reaches {@link runMatchedLanes}. */
|
|
3003
|
+
respondFused(result, ctx, unstamped) {
|
|
3004
|
+
try {
|
|
3005
|
+
return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, unstamped);
|
|
3006
|
+
}
|
|
3007
|
+
catch (err) {
|
|
3008
|
+
return this.bareError(err, ctx, (value, set) => this.wrapWebResponse(toResponse(value, set)), this.wrapWebResponse);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
2802
3011
|
/** Compile the eligible body-only route's parser → validator → handler continuation once. The
|
|
2803
3012
|
* bounded parser remains shared with the generic lane; only the route-invariant entry lookups and
|
|
2804
3013
|
* lifecycle dispatch disappear from the common synchronous-validator/synchronous-handler case. */
|
|
@@ -2867,7 +3076,7 @@ export class Server {
|
|
|
2867
3076
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
2868
3077
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
2869
3078
|
const finish = (value) => runParsed(value, ctx, finalize, wrapResponse);
|
|
2870
|
-
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, finish, wrapResponse, (err) => logError(err, ctx, finalize, wrapResponse), bodySchema);
|
|
3079
|
+
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, finish, wrapResponse, (err) => logError(err, ctx, finalize, wrapResponse), bodySchema, this.formOrigins);
|
|
2871
3080
|
};
|
|
2872
3081
|
}
|
|
2873
3082
|
/** Web adapter wrapper for the shared body runner. Node passes its native finalizer directly through
|
|
@@ -2897,9 +3106,9 @@ export class Server {
|
|
|
2897
3106
|
return logError(err, ctx);
|
|
2898
3107
|
}
|
|
2899
3108
|
if (result instanceof Promise) {
|
|
2900
|
-
return result.then((settled) =>
|
|
3109
|
+
return result.then((settled) => this.respondFused(settled, ctx, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
|
|
2901
3110
|
}
|
|
2902
|
-
return
|
|
3111
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0);
|
|
2903
3112
|
};
|
|
2904
3113
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
2905
3114
|
const ctx = nativeContext
|
|
@@ -2941,173 +3150,88 @@ export class Server {
|
|
|
2941
3150
|
* before → handler in one frame.
|
|
2942
3151
|
*/
|
|
2943
3152
|
buildFusedDeriveBefore(handler, derive, before, querySchema, maxBodyBytes) {
|
|
3153
|
+
return this.buildFusedDeriveBeforeAfter(handler, derive, before, undefined, querySchema, maxBodyBytes);
|
|
3154
|
+
}
|
|
3155
|
+
// Both hook variants share one registered chain; request contexts stay in arguments.
|
|
3156
|
+
buildFusedDeriveBeforeAfter(handler, derive, before, after, querySchema, maxBodyBytes) {
|
|
2944
3157
|
const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
let result;
|
|
2951
|
-
try {
|
|
2952
|
-
result = handler(ctx);
|
|
2953
|
-
}
|
|
2954
|
-
catch (err) {
|
|
2955
|
-
return logError(err, ctx);
|
|
2956
|
-
}
|
|
2957
|
-
if (result instanceof Promise) {
|
|
2958
|
-
return result.then((value) => fusedRespond(value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
|
|
2959
|
-
}
|
|
2960
|
-
return fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
|
|
2961
|
-
};
|
|
2962
|
-
const runBefore = () => {
|
|
2963
|
-
let early;
|
|
2964
|
-
try {
|
|
2965
|
-
early = before(ctx);
|
|
2966
|
-
}
|
|
2967
|
-
catch (err) {
|
|
2968
|
-
return logError(err, ctx);
|
|
2969
|
-
}
|
|
2970
|
-
if (early instanceof Promise) {
|
|
2971
|
-
return early.then((settled) => {
|
|
2972
|
-
if (settled === undefined)
|
|
2973
|
-
return runHandler();
|
|
2974
|
-
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2975
|
-
}, (err) => logError(err, ctx));
|
|
2976
|
-
}
|
|
2977
|
-
if (early === undefined)
|
|
2978
|
-
return runHandler();
|
|
2979
|
-
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2980
|
-
};
|
|
2981
|
-
const runDerive = () => {
|
|
2982
|
-
let derived;
|
|
2983
|
-
try {
|
|
2984
|
-
derived = derive(ctx);
|
|
2985
|
-
}
|
|
2986
|
-
catch (err) {
|
|
2987
|
-
return logError(err, ctx);
|
|
2988
|
-
}
|
|
2989
|
-
if (derived instanceof Promise) {
|
|
2990
|
-
return derived.then((settled) => {
|
|
2991
|
-
if (isResponseResult(settled) || settled instanceof Response) {
|
|
2992
|
-
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
2993
|
-
}
|
|
2994
|
-
Object.assign(ctx, settled);
|
|
2995
|
-
return runBefore();
|
|
2996
|
-
}, (err) => logError(err, ctx));
|
|
2997
|
-
}
|
|
2998
|
-
if (isResponseResult(derived) || derived instanceof Response) {
|
|
2999
|
-
return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
3000
|
-
}
|
|
3001
|
-
Object.assign(ctx, derived);
|
|
3002
|
-
return runBefore();
|
|
3003
|
-
};
|
|
3004
|
-
if (querySchema === undefined)
|
|
3005
|
-
return runDerive();
|
|
3006
|
-
let validation;
|
|
3158
|
+
// Continuations capture route state once; each request supplies its own context.
|
|
3159
|
+
const runAfter = (result, ctx) => {
|
|
3160
|
+
if (after === undefined)
|
|
3161
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0);
|
|
3162
|
+
let transformed;
|
|
3007
3163
|
try {
|
|
3008
|
-
|
|
3164
|
+
transformed = after(result, ctx);
|
|
3009
3165
|
}
|
|
3010
3166
|
catch (err) {
|
|
3011
3167
|
return logError(err, ctx);
|
|
3012
3168
|
}
|
|
3013
|
-
if (
|
|
3014
|
-
return
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3169
|
+
if (transformed instanceof Promise) {
|
|
3170
|
+
return transformed.then((value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
|
|
3171
|
+
}
|
|
3172
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0);
|
|
3173
|
+
};
|
|
3174
|
+
const runHandler = (ctx) => {
|
|
3175
|
+
let result;
|
|
3176
|
+
try {
|
|
3177
|
+
result = handler(ctx);
|
|
3178
|
+
}
|
|
3179
|
+
catch (err) {
|
|
3180
|
+
return logError(err, ctx);
|
|
3181
|
+
}
|
|
3182
|
+
if (result instanceof Promise) {
|
|
3183
|
+
return result.then((value) => runAfter(value, ctx), (err) => logError(err, ctx));
|
|
3184
|
+
}
|
|
3185
|
+
return runAfter(result, ctx);
|
|
3186
|
+
};
|
|
3187
|
+
const runBefore = (ctx) => {
|
|
3188
|
+
let early;
|
|
3189
|
+
try {
|
|
3190
|
+
early = before(ctx);
|
|
3191
|
+
}
|
|
3192
|
+
catch (err) {
|
|
3193
|
+
return logError(err, ctx);
|
|
3194
|
+
}
|
|
3195
|
+
if (early instanceof Promise) {
|
|
3196
|
+
return early.then((settled) => {
|
|
3197
|
+
if (settled === undefined)
|
|
3198
|
+
return runHandler(ctx);
|
|
3199
|
+
return this.respondFused(settled, ctx);
|
|
3019
3200
|
}, (err) => logError(err, ctx));
|
|
3020
3201
|
}
|
|
3021
|
-
if (
|
|
3022
|
-
return
|
|
3023
|
-
|
|
3024
|
-
|
|
3202
|
+
if (early === undefined)
|
|
3203
|
+
return runHandler(ctx);
|
|
3204
|
+
return this.respondFused(early, ctx);
|
|
3205
|
+
};
|
|
3206
|
+
const runDerive = (ctx) => {
|
|
3207
|
+
let derived;
|
|
3208
|
+
try {
|
|
3209
|
+
derived = derive(ctx);
|
|
3210
|
+
}
|
|
3211
|
+
catch (err) {
|
|
3212
|
+
return logError(err, ctx);
|
|
3213
|
+
}
|
|
3214
|
+
if (derived instanceof Promise) {
|
|
3215
|
+
return derived.then((settled) => {
|
|
3216
|
+
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3217
|
+
return this.respondFused(settled, ctx);
|
|
3218
|
+
}
|
|
3219
|
+
Object.assign(ctx, settled);
|
|
3220
|
+
return runBefore(ctx);
|
|
3221
|
+
}, (err) => logError(err, ctx));
|
|
3222
|
+
}
|
|
3223
|
+
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3224
|
+
return this.respondFused(derived, ctx);
|
|
3225
|
+
}
|
|
3226
|
+
Object.assign(ctx, derived);
|
|
3227
|
+
return runBefore(ctx);
|
|
3025
3228
|
};
|
|
3026
|
-
}
|
|
3027
|
-
/**
|
|
3028
|
-
* Same shape as `buildFusedDeriveBefore`, plus one `afterHandle` step. The closure runs (validate) →
|
|
3029
|
-
* derive → before → handler → after in one frame, the shape the realistic middleware route lands on
|
|
3030
|
-
* when it also wants a response transform (e.g. attaching a request id to a successful payload's
|
|
3031
|
-
* headers). `LifecycleExecutionLane` and `selectRouteLanes` guarantee `afterHandle === 1`, the
|
|
3032
|
-
* throw contract is the same as the generic program, and the `after` step is the final transform +
|
|
3033
|
-
* respond the generic program already does.
|
|
3034
|
-
*/
|
|
3035
|
-
buildFusedDeriveBeforeAfter(handler, derive, before, after, querySchema, maxBodyBytes) {
|
|
3036
|
-
const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
|
|
3037
3229
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3038
3230
|
const ctx = nativeContext
|
|
3039
3231
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3040
3232
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
3041
|
-
const runAfter = (result) => {
|
|
3042
|
-
let transformed;
|
|
3043
|
-
try {
|
|
3044
|
-
transformed = after(result, ctx);
|
|
3045
|
-
}
|
|
3046
|
-
catch (err) {
|
|
3047
|
-
return logError(err, ctx);
|
|
3048
|
-
}
|
|
3049
|
-
if (transformed instanceof Promise) {
|
|
3050
|
-
return transformed.then((value) => fusedRespond(value, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
|
|
3051
|
-
}
|
|
3052
|
-
return fusedRespond(transformed, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0);
|
|
3053
|
-
};
|
|
3054
|
-
const runHandler = () => {
|
|
3055
|
-
let result;
|
|
3056
|
-
try {
|
|
3057
|
-
result = handler(ctx);
|
|
3058
|
-
}
|
|
3059
|
-
catch (err) {
|
|
3060
|
-
return logError(err, ctx);
|
|
3061
|
-
}
|
|
3062
|
-
if (result instanceof Promise) {
|
|
3063
|
-
return result.then((value) => runAfter(value), (err) => logError(err, ctx));
|
|
3064
|
-
}
|
|
3065
|
-
return runAfter(result);
|
|
3066
|
-
};
|
|
3067
|
-
const runBefore = () => {
|
|
3068
|
-
let early;
|
|
3069
|
-
try {
|
|
3070
|
-
early = before(ctx);
|
|
3071
|
-
}
|
|
3072
|
-
catch (err) {
|
|
3073
|
-
return logError(err, ctx);
|
|
3074
|
-
}
|
|
3075
|
-
if (early instanceof Promise) {
|
|
3076
|
-
return early.then((settled) => {
|
|
3077
|
-
if (settled === undefined)
|
|
3078
|
-
return runHandler();
|
|
3079
|
-
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
3080
|
-
}, (err) => logError(err, ctx));
|
|
3081
|
-
}
|
|
3082
|
-
if (early === undefined)
|
|
3083
|
-
return runHandler();
|
|
3084
|
-
return fusedRespond(early, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
3085
|
-
};
|
|
3086
|
-
const runDerive = () => {
|
|
3087
|
-
let derived;
|
|
3088
|
-
try {
|
|
3089
|
-
derived = derive(ctx);
|
|
3090
|
-
}
|
|
3091
|
-
catch (err) {
|
|
3092
|
-
return logError(err, ctx);
|
|
3093
|
-
}
|
|
3094
|
-
if (derived instanceof Promise) {
|
|
3095
|
-
return derived.then((settled) => {
|
|
3096
|
-
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3097
|
-
return fusedRespond(settled, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
3098
|
-
}
|
|
3099
|
-
Object.assign(ctx, settled);
|
|
3100
|
-
return runBefore();
|
|
3101
|
-
}, (err) => logError(err, ctx));
|
|
3102
|
-
}
|
|
3103
|
-
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3104
|
-
return fusedRespond(derived, ctx, this.responseBodyTag, this.staticResponseHeaders);
|
|
3105
|
-
}
|
|
3106
|
-
Object.assign(ctx, derived);
|
|
3107
|
-
return runBefore();
|
|
3108
|
-
};
|
|
3109
3233
|
if (querySchema === undefined)
|
|
3110
|
-
return runDerive();
|
|
3234
|
+
return runDerive(ctx);
|
|
3111
3235
|
let validation;
|
|
3112
3236
|
try {
|
|
3113
3237
|
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
|
|
@@ -3120,13 +3244,13 @@ export class Server {
|
|
|
3120
3244
|
if (settled.issues !== undefined)
|
|
3121
3245
|
return this.wrapWebResponse(plainValidationError(settled.issues));
|
|
3122
3246
|
ctx.query = settled.value;
|
|
3123
|
-
return runDerive();
|
|
3247
|
+
return runDerive(ctx);
|
|
3124
3248
|
}, (err) => logError(err, ctx));
|
|
3125
3249
|
}
|
|
3126
3250
|
if (validation.issues !== undefined)
|
|
3127
3251
|
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
3128
3252
|
ctx.query = validation.value;
|
|
3129
|
-
return runDerive();
|
|
3253
|
+
return runDerive(ctx);
|
|
3130
3254
|
};
|
|
3131
3255
|
}
|
|
3132
3256
|
/**
|
|
@@ -3150,7 +3274,7 @@ export class Server {
|
|
|
3150
3274
|
const runHandlerValidated = () => {
|
|
3151
3275
|
const runAfter = (result) => {
|
|
3152
3276
|
if (after === undefined) {
|
|
3153
|
-
return
|
|
3277
|
+
return this.respondFused(result, ctx, this.onResponseHooks.length === 0);
|
|
3154
3278
|
}
|
|
3155
3279
|
let transformed;
|
|
3156
3280
|
try {
|
|
@@ -3160,9 +3284,9 @@ export class Server {
|
|
|
3160
3284
|
return logError(err, ctx);
|
|
3161
3285
|
}
|
|
3162
3286
|
if (transformed instanceof Promise) {
|
|
3163
|
-
return transformed.then((value) =>
|
|
3287
|
+
return transformed.then((value) => this.respondFused(value, ctx, this.onResponseHooks.length === 0), (err) => logError(err, ctx));
|
|
3164
3288
|
}
|
|
3165
|
-
return
|
|
3289
|
+
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0);
|
|
3166
3290
|
};
|
|
3167
3291
|
const runHandler = () => {
|
|
3168
3292
|
let result;
|
|
@@ -3189,12 +3313,12 @@ export class Server {
|
|
|
3189
3313
|
return early.then((settled) => {
|
|
3190
3314
|
if (settled === undefined)
|
|
3191
3315
|
return runHandler();
|
|
3192
|
-
return
|
|
3316
|
+
return this.respondFused(settled, ctx);
|
|
3193
3317
|
}, (err) => logError(err, ctx));
|
|
3194
3318
|
}
|
|
3195
3319
|
if (early === undefined)
|
|
3196
3320
|
return runHandler();
|
|
3197
|
-
return
|
|
3321
|
+
return this.respondFused(early, ctx);
|
|
3198
3322
|
};
|
|
3199
3323
|
let derived;
|
|
3200
3324
|
try {
|
|
@@ -3206,14 +3330,14 @@ export class Server {
|
|
|
3206
3330
|
if (derived instanceof Promise) {
|
|
3207
3331
|
return derived.then((settled) => {
|
|
3208
3332
|
if (isResponseResult(settled) || settled instanceof Response) {
|
|
3209
|
-
return
|
|
3333
|
+
return this.respondFused(settled, ctx);
|
|
3210
3334
|
}
|
|
3211
3335
|
Object.assign(ctx, settled);
|
|
3212
3336
|
return runBefore();
|
|
3213
3337
|
}, (err) => logError(err, ctx));
|
|
3214
3338
|
}
|
|
3215
3339
|
if (isResponseResult(derived) || derived instanceof Response) {
|
|
3216
|
-
return
|
|
3340
|
+
return this.respondFused(derived, ctx);
|
|
3217
3341
|
}
|
|
3218
3342
|
Object.assign(ctx, derived);
|
|
3219
3343
|
return runBefore();
|
|
@@ -3239,7 +3363,7 @@ export class Server {
|
|
|
3239
3363
|
ctx.body = validation.value;
|
|
3240
3364
|
return runHandlerValidated();
|
|
3241
3365
|
};
|
|
3242
|
-
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, onParsed, (response) => this.wrapWebResponse(response), (err) => Promise.resolve(logError(err, ctx)), bodySchema);
|
|
3366
|
+
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, onParsed, (response) => this.wrapWebResponse(response), (err) => Promise.resolve(logError(err, ctx)), bodySchema, this.formOrigins);
|
|
3243
3367
|
};
|
|
3244
3368
|
}
|
|
3245
3369
|
/**
|
|
@@ -3519,7 +3643,7 @@ export class Server {
|
|
|
3519
3643
|
const ctx = nativeContext
|
|
3520
3644
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3521
3645
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
3522
|
-
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => logError(err, ctx, finalize, wrapResponse), bodySchema);
|
|
3646
|
+
return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => logError(err, ctx, finalize, wrapResponse), bodySchema, this.formOrigins);
|
|
3523
3647
|
};
|
|
3524
3648
|
}
|
|
3525
3649
|
bareError(err, ctx, finalize, wrapResponse) {
|
|
@@ -3638,14 +3762,14 @@ export class Server {
|
|
|
3638
3762
|
Object.assign(ctx, entry.decorations);
|
|
3639
3763
|
const handlerOutput = entry.handler(ctx);
|
|
3640
3764
|
if (handlerOutput instanceof Promise) {
|
|
3641
|
-
return handlerOutput.then((value) => finalize(value, responseSet(ctx)), (err) => {
|
|
3765
|
+
return handlerOutput.then((value) => finalize(value, responseSet(ctx), ctx), (err) => {
|
|
3642
3766
|
if (wrapResponse) {
|
|
3643
3767
|
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
3644
3768
|
}
|
|
3645
3769
|
throw err;
|
|
3646
3770
|
});
|
|
3647
3771
|
}
|
|
3648
|
-
return finalize(handlerOutput, responseSet(ctx));
|
|
3772
|
+
return finalize(handlerOutput, responseSet(ctx), ctx);
|
|
3649
3773
|
}
|
|
3650
3774
|
handleValidationErrorRecovery(entry, recovery, ctx, finalize, wrapResponse, originalIssues, kind) {
|
|
3651
3775
|
if (recovery !== undefined) {
|
|
@@ -3751,13 +3875,16 @@ export class Server {
|
|
|
3751
3875
|
*/
|
|
3752
3876
|
applyOnResponse(response, req) {
|
|
3753
3877
|
const hooks = this.onResponseHooks;
|
|
3754
|
-
|
|
3878
|
+
// Hooks write onto the response they are handed (cookies, CORS, request ids), so every response
|
|
3879
|
+
// entering the chain - and any other one a hook hands back - is first made this request's own.
|
|
3880
|
+
let current = writableResponse(response, req);
|
|
3755
3881
|
for (let i = 0; i < hooks.length; i++) {
|
|
3756
3882
|
const next = hooks[i](current, req);
|
|
3757
3883
|
if (next instanceof Promise) {
|
|
3758
|
-
|
|
3884
|
+
const handed = current;
|
|
3885
|
+
return next.then((settled) => this.continueOnResponse(settled, i + 1, req, handed));
|
|
3759
3886
|
}
|
|
3760
|
-
current = next;
|
|
3887
|
+
current = next === current ? next : writableResponse(next, req);
|
|
3761
3888
|
}
|
|
3762
3889
|
return current;
|
|
3763
3890
|
}
|
|
@@ -3816,11 +3943,12 @@ export class Server {
|
|
|
3816
3943
|
return pending;
|
|
3817
3944
|
}
|
|
3818
3945
|
/** Async tail of {@link applyOnResponse}: runs the remaining hooks once one has gone async. */
|
|
3819
|
-
async continueOnResponse(response, nextIndex, req) {
|
|
3820
|
-
let current = response;
|
|
3946
|
+
async continueOnResponse(response, nextIndex, req, handed) {
|
|
3947
|
+
let current = response === handed ? response : writableResponse(response, req);
|
|
3821
3948
|
for (let i = nextIndex; i < this.onResponseHooks.length; i++) {
|
|
3822
3949
|
const next = this.onResponseHooks[i](current, req);
|
|
3823
|
-
|
|
3950
|
+
const settled = next instanceof Promise ? await next : next;
|
|
3951
|
+
current = settled === current ? settled : writableResponse(settled, req);
|
|
3824
3952
|
}
|
|
3825
3953
|
return current;
|
|
3826
3954
|
}
|
|
@@ -3849,7 +3977,7 @@ export class Server {
|
|
|
3849
3977
|
try {
|
|
3850
3978
|
const contract = entry.responseContract;
|
|
3851
3979
|
if (contract === undefined)
|
|
3852
|
-
return finalize(result, responseSet(ctx));
|
|
3980
|
+
return finalize(result, responseSet(ctx), ctx);
|
|
3853
3981
|
const checked = contract.runtime.check(contract.definition, result, ctx);
|
|
3854
3982
|
if (checked instanceof Promise) {
|
|
3855
3983
|
return checked.then((outcome) => this.finishContractOutcome(ctx, finalize, wrapResponse, outcome), (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
@@ -3876,7 +4004,7 @@ export class Server {
|
|
|
3876
4004
|
detail: outcome.message,
|
|
3877
4005
|
});
|
|
3878
4006
|
}
|
|
3879
|
-
return finalize(outcome.value, responseSet(ctx));
|
|
4007
|
+
return finalize(outcome.value, responseSet(ctx), ctx);
|
|
3880
4008
|
}
|
|
3881
4009
|
/** Synchronous by design, and `MaybePromise` rather than `Promise` for the same reason: control flow
|
|
3882
4010
|
* out of a lifecycle stage must not cost a promise. An `async` method allocates one and defers the
|
|
@@ -3889,11 +4017,10 @@ export class Server {
|
|
|
3889
4017
|
// SvelteKit semantics): as built, plus the cookies the request queued, so a session set or
|
|
3890
4018
|
// cleared before `throw redirect(...)` / `requireSession(...)` ships. Don't run onError or log
|
|
3891
4019
|
// it as a 500.
|
|
3892
|
-
if (err instanceof Response)
|
|
3893
|
-
return finalize(err, responseSet(ctx));
|
|
3894
|
-
// Same rule for a thrown `status(...)`,
|
|
3895
|
-
|
|
3896
|
-
if (isResponseResult(err))
|
|
4020
|
+
if (err instanceof Response && err !== ctx.jsonReply)
|
|
4021
|
+
return finalize(err, responseSet(ctx), ctx);
|
|
4022
|
+
// Same rule for a thrown `status(...)` or `c.json(...)`, which stay held to the route's contract.
|
|
4023
|
+
if (err instanceof Response || isResponseResult(err))
|
|
3897
4024
|
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, err);
|
|
3898
4025
|
if (entry.onError.length === 0) {
|
|
3899
4026
|
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
@@ -3911,8 +4038,20 @@ export class Server {
|
|
|
3911
4038
|
for (let i = 0; i < entry.onError.length; i++) {
|
|
3912
4039
|
const outcome = entry.onError[i](err, ctx);
|
|
3913
4040
|
const handled = outcome instanceof Promise ? await outcome : outcome;
|
|
3914
|
-
if (handled
|
|
3915
|
-
|
|
4041
|
+
if (handled === undefined)
|
|
4042
|
+
continue;
|
|
4043
|
+
const contract = entry.responseContract;
|
|
4044
|
+
if (contract === undefined || (handled !== ctx.jsonReply && !isResponseResult(handled)))
|
|
4045
|
+
return finalize(handled, responseSet(ctx), ctx);
|
|
4046
|
+
// Checked here, not through finishLifecycleContract: a throwing schema must not re-enter onError.
|
|
4047
|
+
try {
|
|
4048
|
+
const checked = await contract.runtime.check(contract.definition, handled, ctx);
|
|
4049
|
+
return this.finishContractOutcome(ctx, finalize, wrapResponse, checked);
|
|
4050
|
+
}
|
|
4051
|
+
catch (checkError) {
|
|
4052
|
+
this.logRequestError(checkError, ctx);
|
|
4053
|
+
return wrapResponse(plainError(500, "internal_error"));
|
|
4054
|
+
}
|
|
3916
4055
|
}
|
|
3917
4056
|
this.logRequestError(err, ctx);
|
|
3918
4057
|
return wrapResponse(plainError(500, "internal_error"));
|
|
@@ -3933,7 +4072,7 @@ export class Server {
|
|
|
3933
4072
|
? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, "body"))
|
|
3934
4073
|
: this.applyLifecycleValidation(entry, validation, ctx, "body");
|
|
3935
4074
|
};
|
|
3936
|
-
return readBodyFramed(req, bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, validate, (response) => response, (error) => Promise.reject(error), bodySchema);
|
|
4075
|
+
return readBodyFramed(req, bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, validate, (response) => response, (error) => Promise.reject(error), bodySchema, this.formOrigins);
|
|
3937
4076
|
}
|
|
3938
4077
|
/** Apply validation and its recovery hook on the generic lifecycle lane. Recovery is completed
|
|
3939
4078
|
* before derives/beforeHandle run, matching the body-only and query-only execution lanes. */
|
|
@@ -4003,7 +4142,14 @@ export class Server {
|
|
|
4003
4142
|
return this.applyOnResponseAndFinalize(response, request);
|
|
4004
4143
|
};
|
|
4005
4144
|
const runNative = (request, params) => {
|
|
4006
|
-
|
|
4145
|
+
let outcome;
|
|
4146
|
+
try {
|
|
4147
|
+
outcome = capped(request, params, undefined, signal, budget, peer, true);
|
|
4148
|
+
}
|
|
4149
|
+
catch (err) {
|
|
4150
|
+
// A synchronous render throw, as in runMatchedLanes.
|
|
4151
|
+
outcome = this.contextlessBareError(err, request, params, undefined, signal, budget, peer, this.wrapWebResponse);
|
|
4152
|
+
}
|
|
4007
4153
|
return outcome instanceof Promise
|
|
4008
4154
|
? outcome.then((response) => finishNative(request, response))
|
|
4009
4155
|
: finishNative(request, outcome);
|
|
@@ -4020,7 +4166,7 @@ export class Server {
|
|
|
4020
4166
|
markFramed(request);
|
|
4021
4167
|
return request.headers.get(NIFRA_DEADLINE_HEADER) !== null
|
|
4022
4168
|
? this.fetchMatched(request, entry, EMPTY_PARAMS, peer)
|
|
4023
|
-
:
|
|
4169
|
+
: runNative(request, EMPTY_PARAMS);
|
|
4024
4170
|
};
|
|
4025
4171
|
}
|
|
4026
4172
|
return (request) => {
|
|
@@ -4048,7 +4194,7 @@ export class Server {
|
|
|
4048
4194
|
return fallback(request, server);
|
|
4049
4195
|
return request.headers.get(NIFRA_DEADLINE_HEADER) !== null
|
|
4050
4196
|
? this.fetchMatched(request, entry, params, peer)
|
|
4051
|
-
:
|
|
4197
|
+
: runNative(request, params);
|
|
4052
4198
|
};
|
|
4053
4199
|
}
|
|
4054
4200
|
return (request, server) => {
|
|
@@ -4072,7 +4218,7 @@ export class Server {
|
|
|
4072
4218
|
// bypasses - so an app that declares trust routes through the fetch lane (where `c.clientIp`
|
|
4073
4219
|
// resolves) instead of Bun's native table. The allocation-free default keeps native fusion.
|
|
4074
4220
|
if ((this.onRequestHooks.length > 0 && !this.bunNativeRequestHooksSafe) ||
|
|
4075
|
-
this.
|
|
4221
|
+
this.wsRoutes.length > 0 ||
|
|
4076
4222
|
this.preRouteMountCount > 0 ||
|
|
4077
4223
|
this.clientIpTrust !== undefined) {
|
|
4078
4224
|
return undefined;
|
|
@@ -4208,7 +4354,7 @@ export class Server {
|
|
|
4208
4354
|
}
|
|
4209
4355
|
// Seal before Bun.serve: in `"error"` mode this throws instead of leaving a bound port behind.
|
|
4210
4356
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
4211
|
-
sealHookAudit(this, this.catalog.size, this.logger);
|
|
4357
|
+
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger);
|
|
4212
4358
|
// Bun's `Server` is the concrete handle; we expose the stable `RunningServer`
|
|
4213
4359
|
// subset so the public types don't depend on the ambient `Bun` global. The cast
|
|
4214
4360
|
// bridges them - Bun's `.port` is `number | undefined` (undefined only for unix
|
|
@@ -4219,7 +4365,7 @@ export class Server {
|
|
|
4219
4365
|
// `websocket` handlers are one shared dispatcher - each connection's `ws.data.handler` is the
|
|
4220
4366
|
// matched route's handler, set by `server.upgrade`.
|
|
4221
4367
|
// With WS routes, the dispatcher comes from the installed `.use(websocket())` runtime - non-null
|
|
4222
|
-
// because
|
|
4368
|
+
// because a socket route means ws() ran, and ws() requires the runtime at registration.
|
|
4223
4369
|
// Native pub/sub when the app has WS routes and none validate outbound frames: `ws.subscribe` and
|
|
4224
4370
|
// `app.publish` go through Bun's own (uWebSockets) broadcast instead of the JS registry loop.
|
|
4225
4371
|
// A mounted app counts only while it has a WebSocket route (see `[GET_WS_RUNTIME]`), so composing
|
|
@@ -4232,8 +4378,8 @@ export class Server {
|
|
|
4232
4378
|
// upgrades owns a different TopicRegistry, so force the per-connection JS dispatcher and carry
|
|
4233
4379
|
// that registry in the upgrade outcome instead of accidentally broadcasting child sockets through
|
|
4234
4380
|
// the parent.
|
|
4235
|
-
const nativePubsub = this.
|
|
4236
|
-
const wsHandlers = wsRuntime?.bunHandlers(this.topics ?? wsRuntime.createTopics(), nativePubsub);
|
|
4381
|
+
const nativePubsub = this.wsRoutes.length > 0 && !this.mountsTakeUpgrades() && !this.wsHasValidatedSend;
|
|
4382
|
+
const wsHandlers = wsRuntime?.bunHandlers(this.topics ?? wsRuntime.createTopics(), nativePubsub, this);
|
|
4237
4383
|
// Spread rather than pass `hostname: undefined` (or an undefined idle timeout or TLS) - Bun treats
|
|
4238
4384
|
// an explicit undefined as a value on some option paths, and omitting is what selects its own
|
|
4239
4385
|
// defaults, 0.0.0.0 among them.
|
|
@@ -4279,8 +4425,9 @@ export class Server {
|
|
|
4279
4425
|
// `unknown` params bridge a TS structural-variance quirk. Round-trip covered by websocket.test.ts.
|
|
4280
4426
|
websocket: {
|
|
4281
4427
|
// Cap inbound frames so a huge message can't be buffered/JSON-parsed into memory; the runtime
|
|
4282
|
-
// closes an over-cap connection before the handler runs. Default 1 MB (maxBodyBytes).
|
|
4283
|
-
|
|
4428
|
+
// closes an over-cap connection before the handler runs. Default 1 MB (maxBodyBytes). One
|
|
4429
|
+
// limit per listener: the largest cap among this app and the apps mounted on it.
|
|
4430
|
+
maxPayloadLength: wsHandlers.maxPayloadLength,
|
|
4284
4431
|
open: (ws) => wsHandlers.open(ws),
|
|
4285
4432
|
message: (ws, message) => wsHandlers.message(ws, message),
|
|
4286
4433
|
close: (ws, code, reason) => wsHandlers.close(ws, code, reason),
|
|
@@ -4299,7 +4446,7 @@ export class Server {
|
|
|
4299
4446
|
}
|
|
4300
4447
|
this.sealed = true;
|
|
4301
4448
|
if (this.gracefulSignals)
|
|
4302
|
-
this.installSignalHandlers();
|
|
4449
|
+
this.installSignalHandlers(this.gracefulSignals);
|
|
4303
4450
|
return running;
|
|
4304
4451
|
}
|
|
4305
4452
|
/**
|
|
@@ -4332,12 +4479,12 @@ export class Server {
|
|
|
4332
4479
|
new Promise((resolve) => setTimeout(resolve, STOP_HOOK_TIMEOUT_MS)),
|
|
4333
4480
|
]);
|
|
4334
4481
|
}
|
|
4335
|
-
installSignalHandlers() {
|
|
4482
|
+
installSignalHandlers(drain) {
|
|
4336
4483
|
// Drain, then let the process exit naturally - the stopped server no longer
|
|
4337
4484
|
// holds the event loop open. Opt-in (`gracefulSignals`), so taking over the
|
|
4338
4485
|
// signals is consented; we don't force `process.exit`.
|
|
4339
4486
|
const onSignal = () => {
|
|
4340
|
-
void this.stop();
|
|
4487
|
+
void this.stop(drain);
|
|
4341
4488
|
};
|
|
4342
4489
|
process.once("SIGTERM", onSignal);
|
|
4343
4490
|
process.once("SIGINT", onSignal);
|