@nifrajs/core 5.11.0 → 5.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/edge-kit.d.ts +1 -1
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +1 -1
- package/dist/edge-kit.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/capability-runtime.d.ts.map +1 -1
- package/dist/internal/capability-runtime.js +3 -2
- package/dist/internal/capability-runtime.js.map +1 -1
- package/dist/internal/route-execution.d.ts +6 -4
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +4 -4
- package/dist/internal/route-lanes.d.ts.map +1 -1
- package/dist/internal/route-lanes.js +37 -37
- package/dist/internal/route-lanes.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +2 -1
- package/dist/manifest.js.map +1 -1
- package/dist/server/body.d.ts +2 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +2 -0
- package/dist/server/body.js.map +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/server-types.d.ts +7 -1
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +16 -24
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +285 -140
- package/dist/server/server.js.map +1 -1
- package/dist/server/static-response.d.ts +13 -0
- package/dist/server/static-response.d.ts.map +1 -0
- package/dist/server/static-response.js +28 -0
- package/dist/server/static-response.js.map +1 -0
- package/dist/server/websocket.d.ts +23 -7
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +8 -3
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-adapter-view.d.ts +3 -3
- package/dist/server/ws-adapter-view.d.ts.map +1 -1
- package/dist/server/ws-bun.d.ts +4 -4
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +12 -5
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +4 -4
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/tool-contract.d.ts +5 -0
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +6 -3
- package/dist/tool-contract.js.map +1 -1
- package/dist/ws.d.ts +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/edge-kit.ts +1 -1
- package/src/index.ts +3 -1
- package/src/internal/capability-runtime.ts +3 -2
- package/src/internal/route-execution.ts +6 -4
- package/src/internal/route-lanes.ts +44 -44
- package/src/manifest.ts +2 -1
- package/src/server/body.ts +3 -0
- package/src/server/core-copies.ts +1 -1
- package/src/server/server-types.ts +7 -1
- package/src/server/server.ts +414 -189
- package/src/server/static-response.ts +36 -0
- package/src/server/websocket.ts +29 -8
- package/src/server/ws-adapter-view.ts +3 -3
- package/src/server/ws-bun.ts +15 -9
- package/src/server/ws-hook.ts +4 -3
- package/src/server.ts +2 -0
- package/src/tool-contract.ts +13 -3
- package/src/ws.ts +1 -0
package/dist/server/server.js
CHANGED
|
@@ -14,11 +14,11 @@ import { withAnswerSteps } from "./answer-step.js";
|
|
|
14
14
|
import { isAuthenticatedRequest, markAuthenticatedRequest, } from "./auth.js";
|
|
15
15
|
import { emitRequestErrorLog, renderBareError } from "./bare-error-lane.js";
|
|
16
16
|
import { LAST_BEFORE_HANDLE } from "./before-handler.js";
|
|
17
|
-
import { assertByteLimit, clientBodyErrorCode, hasTrustedBodyFraming, markTransportCap, markTrustedBodyFraming, UNLIMITED_BODY_BYTES, } from "./body.js";
|
|
17
|
+
import { assertByteLimit, clientBodyErrorCode, DEFAULT_MAX_BODY_BYTES, hasTrustedBodyFraming, markTransportCap, markTrustedBodyFraming, UNLIMITED_BODY_BYTES, } from "./body.js";
|
|
18
18
|
import { NIFRA_PLATFORM_CLIENT_IP_DERIVED, resolveClientIp, } from "./client-ip.js";
|
|
19
19
|
import { mergeRefusal, recordCopyVersion } from "./core-copies.js";
|
|
20
20
|
import { hasLowercaseHeaderKeysMark, headerKeysAllLowercase, markLowercaseHeaderKeys, } from "./header-case.js";
|
|
21
|
-
import { declaredHeaders, headerObjectOf } from "./headers.js";
|
|
21
|
+
import { declaredHeaderNames, declaredHeaders, headerObjectOf } from "./headers.js";
|
|
22
22
|
import { isRoutableMethod, jsonError, pathnameOf, plainError, urlPartsOf, } from "./http.js";
|
|
23
23
|
import { withStaticNodeHeaders } from "./node-outcome.js";
|
|
24
24
|
import { queryObjectOf, searchOf } from "./query.js";
|
|
@@ -29,6 +29,7 @@ import { needsResolving, resolveDotSegments } from "./request-target.js";
|
|
|
29
29
|
import { applyStaticResponseHeaders, buildStaticResponseHeaders, claimHandedOver, fusedRespond, fusedRespondNoSet, handedBackResponse, recordResidentTexts, toResponse, writableResponse, } from "./respond.js";
|
|
30
30
|
import { CONTEXT_REQUEST, CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, } from "./runtime-core.js";
|
|
31
31
|
import { normalizeStaticResponseHeaders } from "./static-headers.js";
|
|
32
|
+
import { createStaticResponse, staticResponseDefinition } from "./static-response.js";
|
|
32
33
|
import { withTextSlots } from "./text-slots.js";
|
|
33
34
|
import { plainValidationError } from "./validation.js";
|
|
34
35
|
import { NIFRA_BACKEND_WS_RUNTIME } from "../mount.js";
|
|
@@ -155,7 +156,6 @@ function requireMcpRuntime(runtime) {
|
|
|
155
156
|
// Plugin definers + their types now live in `./plugin.ts`; re-exported here so `.use()` callers and
|
|
156
157
|
// existing importers keep resolving them from the server module.
|
|
157
158
|
export { defineContextPlugin, defineIdentityPlugin, definePlugin, defineRouterPlugin, } from "./plugin.js";
|
|
158
|
-
const DEFAULT_MAX_BODY_BYTES = 1_000_000;
|
|
159
159
|
const BODY_RESPONSE = (response) => response;
|
|
160
160
|
const BODY_ERROR = (error) => Promise.reject(error);
|
|
161
161
|
const DEFAULT_DRAIN_MS = 10_000;
|
|
@@ -359,6 +359,78 @@ function stripMountPrefix(request, prefix) {
|
|
|
359
359
|
function responseSet(ctx) {
|
|
360
360
|
return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS;
|
|
361
361
|
}
|
|
362
|
+
/** Compile the non-body validation stages in the same order as the route program. The selected
|
|
363
|
+
* fused lane runs this only for routes whose recovery behavior is the ordinary 422 response. */
|
|
364
|
+
function compileFusedValidationStages(schema) {
|
|
365
|
+
if (schema === undefined)
|
|
366
|
+
return [];
|
|
367
|
+
const stages = [];
|
|
368
|
+
if (schema.headers !== undefined) {
|
|
369
|
+
stages.push({
|
|
370
|
+
schema: schema.headers,
|
|
371
|
+
kind: "headers",
|
|
372
|
+
headerNames: declaredHeaderNames(schema.headers),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (schema.cookies !== undefined) {
|
|
376
|
+
stages.push({ schema: schema.cookies, kind: "cookies" });
|
|
377
|
+
}
|
|
378
|
+
if (schema.params !== undefined) {
|
|
379
|
+
stages.push({ schema: schema.params, kind: "params" });
|
|
380
|
+
}
|
|
381
|
+
if (schema.query !== undefined) {
|
|
382
|
+
stages.push({ schema: schema.query, kind: "query" });
|
|
383
|
+
}
|
|
384
|
+
return stages;
|
|
385
|
+
}
|
|
386
|
+
/** Run a registration-compiled input sequence without allocating a lifecycle-program continuation
|
|
387
|
+
* for each synchronous schema. Only an asynchronous validator creates a Promise continuation. */
|
|
388
|
+
function runFusedValidationStages(stages, source, ctx, wrapResponse, onError, next, startIndex = 0) {
|
|
389
|
+
if (stages.length === 0)
|
|
390
|
+
return next();
|
|
391
|
+
const validationContext = ctx;
|
|
392
|
+
for (let index = startIndex; index < stages.length; index++) {
|
|
393
|
+
const stage = stages[index];
|
|
394
|
+
let validation;
|
|
395
|
+
try {
|
|
396
|
+
const input = stage.kind === "headers"
|
|
397
|
+
? stage.headerNames === undefined
|
|
398
|
+
? headerObjectOf(source.headers)
|
|
399
|
+
: declaredHeaders(source.headers, stage.headerNames)
|
|
400
|
+
: stage.kind === "cookies"
|
|
401
|
+
? ctx.cookies
|
|
402
|
+
: stage.kind === "params"
|
|
403
|
+
? ctx.params
|
|
404
|
+
: queryObjectOf(ctx[CONTEXT_SEARCH]);
|
|
405
|
+
validation = stage.schema["~standard"].validate(input);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
return onError(error);
|
|
409
|
+
}
|
|
410
|
+
if (validation instanceof Promise) {
|
|
411
|
+
return validation.then((result) => {
|
|
412
|
+
if (result.issues !== undefined)
|
|
413
|
+
return wrapResponse(plainValidationError(result.issues));
|
|
414
|
+
try {
|
|
415
|
+
validationContext[stage.kind] = result.value;
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
return onError(error);
|
|
419
|
+
}
|
|
420
|
+
return runFusedValidationStages(stages, source, ctx, wrapResponse, onError, next, index + 1);
|
|
421
|
+
}, onError);
|
|
422
|
+
}
|
|
423
|
+
if (validation.issues !== undefined)
|
|
424
|
+
return wrapResponse(plainValidationError(validation.issues));
|
|
425
|
+
try {
|
|
426
|
+
validationContext[stage.kind] = validation.value;
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
return onError(error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return next();
|
|
433
|
+
}
|
|
362
434
|
/** The error hooks of the many fused routes that have none, shared so each does not allocate its own. */
|
|
363
435
|
const NO_ERROR_HOOKS = [];
|
|
364
436
|
const WEB_STAGES = new WeakMap();
|
|
@@ -701,17 +773,15 @@ export class Server {
|
|
|
701
773
|
wsMountsCheckedAt = -1;
|
|
702
774
|
/** Whether an app mounted here takes WebSocket upgrades, as of `wsMountsCheckedAt`. */
|
|
703
775
|
wsMountsLive = false;
|
|
704
|
-
/**
|
|
705
|
-
* Created by the first `app.ws()` via the `@nifrajs/core/ws` runtime - `undefined` until then, so a
|
|
706
|
-
* no-WebSocket app never constructs (or bundles) it. */
|
|
776
|
+
/** Configured topic adapter, or the in-process default created by the first `app.ws()` route. */
|
|
707
777
|
topics;
|
|
708
778
|
/** True once any `app.ws()` route sets `validateSend: true` - the one case where a broadcast frame
|
|
709
|
-
* is validated (and possibly dropped) per socket, so `app.publish` must stay on the JS
|
|
779
|
+
* is validated (and possibly dropped) per socket, so `app.publish` must stay on the JS adapter loop
|
|
710
780
|
* rather than delegate to Bun's native `server.publish`. */
|
|
711
781
|
wsHasValidatedSend = false;
|
|
712
782
|
/** Bun's native topic broadcast, bound by `listen()` in native-pubsub mode; `app.publish` uses it
|
|
713
|
-
* instead of the JS registry. `undefined` off Bun, before `listen()`, with a
|
|
714
|
-
* or when a mounted app takes upgrades. */
|
|
783
|
+
* instead of the JS registry. `undefined` off Bun, before `listen()`, with a custom topic adapter,
|
|
784
|
+
* a validated-send route, or when a mounted app takes upgrades. */
|
|
715
785
|
nativePublish;
|
|
716
786
|
maxBodyBytes;
|
|
717
787
|
protoPoisoning;
|
|
@@ -846,7 +916,7 @@ export class Server {
|
|
|
846
916
|
this.catalog = new RouteCatalog();
|
|
847
917
|
this.wsRouter = new Router();
|
|
848
918
|
this.wsRoutes = [];
|
|
849
|
-
this.topics =
|
|
919
|
+
this.topics = options.topicAdapter;
|
|
850
920
|
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
851
921
|
assertByteLimit(maxBodyBytes, "maxBodyBytes");
|
|
852
922
|
this.protoPoisoning = options.protoPoisoning ?? "reject";
|
|
@@ -1573,7 +1643,7 @@ export class Server {
|
|
|
1573
1643
|
throw new RouteConfigError("INVALID_WS_SEND_VALIDATION", `route WS ${path}: validateSend requires sendSchema`);
|
|
1574
1644
|
}
|
|
1575
1645
|
// A validated-send route makes broadcast bytes route-dependent (drop-on-invalid per socket), so the
|
|
1576
|
-
// whole app forgoes native pub/sub and keeps the JS
|
|
1646
|
+
// whole app forgoes native pub/sub and keeps the JS adapter loop that runs each send through the sender.
|
|
1577
1647
|
if (handler.validateSend === true)
|
|
1578
1648
|
this.wsHasValidatedSend = true;
|
|
1579
1649
|
this.topics ??= runtime.createTopics();
|
|
@@ -1598,18 +1668,18 @@ export class Server {
|
|
|
1598
1668
|
}
|
|
1599
1669
|
/**
|
|
1600
1670
|
* Broadcast `data` to every WebSocket connection subscribed to `topic` (via `ws.subscribe(topic)`).
|
|
1601
|
-
*
|
|
1602
|
-
*
|
|
1671
|
+
* Uses the configured {@link ServerOptions.topicAdapter}, or the in-process topic registry by
|
|
1672
|
+
* default. Await the result when a custom adapter publishes asynchronously.
|
|
1603
1673
|
*/
|
|
1604
1674
|
publish(topic, data) {
|
|
1605
1675
|
// Native-pubsub mode (Bun, no validated-send route): one native broadcast to every subscriber, no
|
|
1606
|
-
// per-connection JS loop. Bound only after `listen()`; before that (or off Bun) the
|
|
1676
|
+
// per-connection JS loop. Bound only after `listen()`; before that (or off Bun) the adapter runs.
|
|
1607
1677
|
if (this.nativePublish !== undefined) {
|
|
1608
1678
|
this.nativePublish(topic, data);
|
|
1609
1679
|
return;
|
|
1610
1680
|
}
|
|
1611
|
-
//
|
|
1612
|
-
this.topics?.publish(topic, data);
|
|
1681
|
+
// With no custom adapter and no `app.ws()` route, no registry exists and this is a no-op.
|
|
1682
|
+
return this.topics?.publish(topic, data);
|
|
1613
1683
|
}
|
|
1614
1684
|
route(method, path, schemaOrHandler, handler) {
|
|
1615
1685
|
let rawHandler;
|
|
@@ -1665,6 +1735,7 @@ export class Server {
|
|
|
1665
1735
|
throw new RouteConfigError("INVALID_PATH", `group(${JSON.stringify(prefix)}) route path must start with "/": ${JSON.stringify(path)}`);
|
|
1666
1736
|
}
|
|
1667
1737
|
prepareRoute(method, path, schema, handler) {
|
|
1738
|
+
const staticResponse = staticResponseDefinition(handler);
|
|
1668
1739
|
schema = withTextSlots(schema);
|
|
1669
1740
|
// Route provenance for the CLI's capability walk, which sets NIFRA_ROUTE_SITES while it loads the
|
|
1670
1741
|
// app: the module that registered the route, read where a static scan cannot see the path.
|
|
@@ -1708,9 +1779,11 @@ export class Server {
|
|
|
1708
1779
|
handler = this.idempotencyRuntime.wrapHandler(handler);
|
|
1709
1780
|
}
|
|
1710
1781
|
const { bare, fusedQuery, fusedBody } = lanes;
|
|
1711
|
-
//
|
|
1712
|
-
//
|
|
1713
|
-
const fusedLifecycle = lanes.fusedLane === "hooks" ||
|
|
1782
|
+
// Composite-input routes and hook-only routes share the lifecycle builders; simple shapes use
|
|
1783
|
+
// their narrower runners.
|
|
1784
|
+
const fusedLifecycle = lanes.fusedLane === "hooks" ||
|
|
1785
|
+
lanes.fusedLane === "inputs" ||
|
|
1786
|
+
(lanes.fusedLane === "query" && schema?.params !== undefined);
|
|
1714
1787
|
// The fused chains keep the arrays they are built from, and `beforeHandle()` inserts into the live
|
|
1715
1788
|
// ones ahead of `beforeHandler()` guards, so the entry and the fused chains share one snapshot of
|
|
1716
1789
|
// this route's.
|
|
@@ -1731,18 +1804,18 @@ export class Server {
|
|
|
1731
1804
|
// The Node-direct dispatcher (`!webFast`) prefers these runners over the generic program. Only the
|
|
1732
1805
|
// body-only runner also feeds the Web wrapper; the lifecycle lanes build their own Web closure.
|
|
1733
1806
|
const fusedBodyRunner = fusedBody
|
|
1734
|
-
? this.buildFusedBodyRunner(handler, schema?.body, hasDecorations ? routeDecorations : undefined, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1807
|
+
? this.buildFusedBodyRunner(handler, schema?.body, schema?.params, hasDecorations ? routeDecorations : undefined, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1735
1808
|
: fusedHooks !== undefined
|
|
1736
|
-
? this.buildFusedLifecycleRunner(fusedHooks, schema
|
|
1809
|
+
? this.buildFusedLifecycleRunner(fusedHooks, schema, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1737
1810
|
: undefined;
|
|
1738
1811
|
const fusedWeb = bare && this.aroundHooks.length === 0
|
|
1739
1812
|
? this.buildFusedWeb(handler, hasDecorations ? routeDecorations : undefined, isContextlessNoArgArrow(handler), bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1740
1813
|
: fusedQuery
|
|
1741
|
-
? this.buildFusedQueryWeb(handler, hasDecorations ? routeDecorations : undefined, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1814
|
+
? this.buildFusedQueryWeb(handler, hasDecorations ? routeDecorations : undefined, schema?.query, schema?.params, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1742
1815
|
: fusedBody
|
|
1743
1816
|
? this.buildFusedBodyWeb(fusedBodyRunner)
|
|
1744
1817
|
: fusedHooks !== undefined
|
|
1745
|
-
? this.buildFusedLifecycleWeb(fusedHooks, schema
|
|
1818
|
+
? this.buildFusedLifecycleWeb(fusedHooks, schema, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1746
1819
|
: undefined;
|
|
1747
1820
|
const program = compileRouteProgram({
|
|
1748
1821
|
schema,
|
|
@@ -1770,6 +1843,7 @@ export class Server {
|
|
|
1770
1843
|
});
|
|
1771
1844
|
const registeredEntry = {
|
|
1772
1845
|
handler: handler,
|
|
1846
|
+
staticResponse,
|
|
1773
1847
|
schema,
|
|
1774
1848
|
bodyLimit,
|
|
1775
1849
|
idempotent,
|
|
@@ -2182,7 +2256,9 @@ export class Server {
|
|
|
2182
2256
|
const lane = entry.execution.fusedLane;
|
|
2183
2257
|
// Type-erase the lane so each `===` branch sees the full union, not the narrowed remainder.
|
|
2184
2258
|
const laneName = lane;
|
|
2185
|
-
const lifecycle = laneName === "hooks" ||
|
|
2259
|
+
const lifecycle = laneName === "hooks" ||
|
|
2260
|
+
laneName === "inputs" ||
|
|
2261
|
+
(laneName === "query" && entry.schema?.params !== undefined);
|
|
2186
2262
|
const hooks = lifecycle
|
|
2187
2263
|
? {
|
|
2188
2264
|
handler: entry.handler,
|
|
@@ -2194,16 +2270,16 @@ export class Server {
|
|
|
2194
2270
|
}
|
|
2195
2271
|
: undefined;
|
|
2196
2272
|
const fusedBody = laneName === "body"
|
|
2197
|
-
? this.buildFusedBodyRunner(entry.handler, entry.schema?.body, entry.hasDecorations ? entry.decorations : undefined, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2273
|
+
? this.buildFusedBodyRunner(entry.handler, entry.schema?.body, entry.schema?.params, entry.hasDecorations ? entry.decorations : undefined, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2198
2274
|
: hooks !== undefined
|
|
2199
|
-
? this.buildFusedLifecycleRunner(hooks, entry.schema
|
|
2275
|
+
? this.buildFusedLifecycleRunner(hooks, entry.schema, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2200
2276
|
: undefined;
|
|
2201
2277
|
const fusedWeb = laneName === "body"
|
|
2202
2278
|
? this.buildFusedBodyWeb(fusedBody)
|
|
2203
2279
|
: laneName === "query"
|
|
2204
|
-
? this.buildFusedQueryWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, entry.schema?.query, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2280
|
+
? this.buildFusedQueryWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, entry.schema?.query, entry.schema?.params, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2205
2281
|
: hooks !== undefined
|
|
2206
|
-
? this.buildFusedLifecycleWeb(hooks, entry.schema
|
|
2282
|
+
? this.buildFusedLifecycleWeb(hooks, entry.schema, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2207
2283
|
: this.buildFusedWeb(entry.handler, entry.hasDecorations ? entry.decorations : undefined, isContextlessNoArgArrow(entry.handler), entry.bodyLimit ?? UNLIMITED_BODY_BYTES);
|
|
2208
2284
|
return {
|
|
2209
2285
|
...route,
|
|
@@ -2704,7 +2780,7 @@ export class Server {
|
|
|
2704
2780
|
if (params === null)
|
|
2705
2781
|
return refuse(jsonError(400, "malformed_path"));
|
|
2706
2782
|
const { handler, chain } = match.payload;
|
|
2707
|
-
// Non-null: a socket route ⇒ ws() ran ⇒ `.use(websocket())` installed the runtime +
|
|
2783
|
+
// Non-null: a socket route ⇒ ws() ran ⇒ `.use(websocket())` installed the runtime + adapter.
|
|
2708
2784
|
const pubsub = this.topics;
|
|
2709
2785
|
const runtime = this.wsRuntime;
|
|
2710
2786
|
// CSWSH guard, before any per-connection work or the user's upgrade(): reject a disallowed
|
|
@@ -3466,10 +3542,10 @@ export class Server {
|
|
|
3466
3542
|
return this.bareError(err, ctx, (value, set) => this.wrapWebResponse(toResponse(value, set)), this.wrapWebResponse);
|
|
3467
3543
|
}
|
|
3468
3544
|
}
|
|
3469
|
-
/** Compile the eligible body
|
|
3545
|
+
/** Compile the eligible body route's params → parser → validator → handler continuation once. The
|
|
3470
3546
|
* bounded parser remains shared with the generic lane; only the route-invariant entry lookups and
|
|
3471
3547
|
* lifecycle dispatch disappear from the common synchronous-validator/synchronous-handler case. */
|
|
3472
|
-
buildFusedBodyRunner(handler, bodySchema, decorations, maxBodyBytes) {
|
|
3548
|
+
buildFusedBodyRunner(handler, bodySchema, paramsSchema, decorations, maxBodyBytes) {
|
|
3473
3549
|
const logError = (err, ctx, finalize, wrapResponse) => this.bareError(err, ctx, finalize, wrapResponse);
|
|
3474
3550
|
const runHandler = (ctx, finalize, wrapResponse) => {
|
|
3475
3551
|
if (decorations !== undefined)
|
|
@@ -3529,12 +3605,34 @@ export class Server {
|
|
|
3529
3605
|
return logError(err, ctx, finalize, wrapResponse);
|
|
3530
3606
|
}
|
|
3531
3607
|
};
|
|
3608
|
+
const readBody = (source, ctx, finalize, wrapResponse) => readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => runParsed(parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => logError(err, ctx, finalize, wrapResponse), bodySchema, this.formOrigins);
|
|
3532
3609
|
return (source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse) => {
|
|
3533
3610
|
const ctx = nativeContext
|
|
3534
3611
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3535
3612
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
3536
|
-
|
|
3537
|
-
|
|
3613
|
+
if (paramsSchema === undefined)
|
|
3614
|
+
return readBody(source, ctx, finalize, wrapResponse);
|
|
3615
|
+
let validation;
|
|
3616
|
+
try {
|
|
3617
|
+
validation = paramsSchema["~standard"].validate(ctx.params);
|
|
3618
|
+
}
|
|
3619
|
+
catch (err) {
|
|
3620
|
+
return logError(err, ctx, finalize, wrapResponse);
|
|
3621
|
+
}
|
|
3622
|
+
if (validation instanceof Promise) {
|
|
3623
|
+
return validation.then((settled) => {
|
|
3624
|
+
if (settled.issues !== undefined)
|
|
3625
|
+
return wrapResponse(plainValidationError(settled.issues));
|
|
3626
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the route's params schema defines this validated output; RawContext erases its route-specific type.
|
|
3627
|
+
ctx.params = settled.value;
|
|
3628
|
+
return readBody(source, ctx, finalize, wrapResponse);
|
|
3629
|
+
}, (err) => logError(err, ctx, finalize, wrapResponse));
|
|
3630
|
+
}
|
|
3631
|
+
if (validation.issues !== undefined)
|
|
3632
|
+
return wrapResponse(plainValidationError(validation.issues));
|
|
3633
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the route's params schema defines this validated output; RawContext erases its route-specific type.
|
|
3634
|
+
ctx.params = validation.value;
|
|
3635
|
+
return readBody(source, ctx, finalize, wrapResponse);
|
|
3538
3636
|
};
|
|
3539
3637
|
}
|
|
3540
3638
|
/** Web adapter wrapper for the shared body runner. Node passes its native finalizer directly through
|
|
@@ -3542,20 +3640,16 @@ export class Server {
|
|
|
3542
3640
|
buildFusedBodyWeb(body) {
|
|
3543
3641
|
return (source, params, search, signal, budget, platform, nativeContext) => body(source, params, search, signal, budget, platform, nativeContext, (result, _set, ctx) => fusedRespond(result, ctx, this.responseBodyTag, this.staticResponseHeaders, this.onResponseHooks.length === 0), this.wrapWebResponse);
|
|
3544
3642
|
}
|
|
3545
|
-
/** The fused Web lane for
|
|
3546
|
-
*
|
|
3547
|
-
|
|
3548
|
-
* are exactly `runQueryOnly`'s for that eligible shape - invalid input returns
|
|
3549
|
-
* `validationError(issues)` (recovery hooks disqualify the route from this lane), a thrown
|
|
3550
|
-
* `Response` passes through, anything else logs and returns a flat 500, and an async validator
|
|
3551
|
-
* falls to a then-chain with the same steps. */
|
|
3552
|
-
buildFusedQueryWeb(handler, decorations, querySchema, maxBodyBytes) {
|
|
3643
|
+
/** The fused Web lane for routes with path params and/or a query schema: validate + handler + respond
|
|
3644
|
+
* in one closure, with no lifecycle promise when the validators and handler are sync. */
|
|
3645
|
+
buildFusedQueryWeb(handler, decorations, querySchema, paramsSchema, maxBodyBytes) {
|
|
3553
3646
|
// Same thrown-value contract as the generic lane (`bare-error-lane.ts` via `bareError`); the fused
|
|
3554
3647
|
// lane only injects its renderer, so a thrown `status(...)` and the flat 500 fold in static headers
|
|
3555
3648
|
// through `wrapWebResponse`. One source of truth for the error surface, no per-lane copy to drift.
|
|
3556
3649
|
const logError = (err, ctx) => this.bareError(err, ctx, (result, set) => this.wrapWebResponse(toResponse(result, set)), this.wrapWebResponse);
|
|
3557
3650
|
const runHandler = (ctx, value) => {
|
|
3558
|
-
|
|
3651
|
+
if (querySchema !== undefined)
|
|
3652
|
+
ctx.query = value;
|
|
3559
3653
|
let result;
|
|
3560
3654
|
try {
|
|
3561
3655
|
result = handler(ctx);
|
|
@@ -3568,27 +3662,54 @@ export class Server {
|
|
|
3568
3662
|
}
|
|
3569
3663
|
return this.respondFused(result, ctx, this.onResponseHooks.length === 0);
|
|
3570
3664
|
};
|
|
3665
|
+
const runQuery = (ctx) => {
|
|
3666
|
+
if (querySchema === undefined)
|
|
3667
|
+
return runHandler(ctx, undefined);
|
|
3668
|
+
let validation;
|
|
3669
|
+
try {
|
|
3670
|
+
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
|
|
3671
|
+
}
|
|
3672
|
+
catch (err) {
|
|
3673
|
+
return logError(err, ctx);
|
|
3674
|
+
}
|
|
3675
|
+
if (validation instanceof Promise) {
|
|
3676
|
+
return validation.then((settled) => settled.issues !== undefined
|
|
3677
|
+
? this.wrapWebResponse(plainValidationError(settled.issues))
|
|
3678
|
+
: runHandler(ctx, settled.value), (err) => logError(err, ctx));
|
|
3679
|
+
}
|
|
3680
|
+
if (validation.issues !== undefined)
|
|
3681
|
+
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
3682
|
+
return runHandler(ctx, validation.value);
|
|
3683
|
+
};
|
|
3571
3684
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3572
3685
|
const ctx = nativeContext
|
|
3573
3686
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3574
3687
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
3575
3688
|
if (decorations !== undefined)
|
|
3576
3689
|
Object.assign(ctx, decorations);
|
|
3690
|
+
if (paramsSchema === undefined)
|
|
3691
|
+
return runQuery(ctx);
|
|
3577
3692
|
let validation;
|
|
3578
3693
|
try {
|
|
3579
|
-
validation =
|
|
3694
|
+
validation = paramsSchema["~standard"].validate(ctx.params);
|
|
3580
3695
|
}
|
|
3581
3696
|
catch (err) {
|
|
3582
3697
|
return logError(err, ctx);
|
|
3583
3698
|
}
|
|
3584
3699
|
if (validation instanceof Promise) {
|
|
3585
|
-
return validation.then((settled) =>
|
|
3586
|
-
|
|
3587
|
-
|
|
3700
|
+
return validation.then((settled) => {
|
|
3701
|
+
if (settled.issues !== undefined)
|
|
3702
|
+
return this.wrapWebResponse(plainValidationError(settled.issues));
|
|
3703
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the route's params schema defines this validated output; RawContext erases its route-specific type.
|
|
3704
|
+
ctx.params = settled.value;
|
|
3705
|
+
return runQuery(ctx);
|
|
3706
|
+
}, (err) => logError(err, ctx));
|
|
3588
3707
|
}
|
|
3589
3708
|
if (validation.issues !== undefined)
|
|
3590
3709
|
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
3591
|
-
|
|
3710
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the route's params schema defines this validated output; RawContext erases its route-specific type.
|
|
3711
|
+
ctx.params = validation.value;
|
|
3712
|
+
return runQuery(ctx);
|
|
3592
3713
|
};
|
|
3593
3714
|
}
|
|
3594
3715
|
/**
|
|
@@ -3792,13 +3913,12 @@ export class Server {
|
|
|
3792
3913
|
/**
|
|
3793
3914
|
* Compile a fused lifecycle route's entry closure: parse (body) → validate → the shared stage chain
|
|
3794
3915
|
* of its hook shape (derive → before → handler → after → respond, see {@link fusedWebStages}) in a
|
|
3795
|
-
* single frame, where the generic program walks staged hook loops. `selectRouteLanes` admits
|
|
3796
|
-
*
|
|
3797
|
-
*
|
|
3798
|
-
*
|
|
3799
|
-
*
|
|
3800
|
-
*
|
|
3801
|
-
* an argument, so a request allocates no step closures.
|
|
3916
|
+
* single frame, where the generic program walks staged hook loops. `selectRouteLanes` admits ordinary
|
|
3917
|
+
* validation stages and derive/before/after/error hooks, while excluding auth-first ordering, custom
|
|
3918
|
+
* validation recovery, outer wrappers and response contracts. The shared input sequence preserves
|
|
3919
|
+
* the program's validation order around body parsing; the hook chain preserves thrown-value, `c.set`
|
|
3920
|
+
* and response-rendering semantics. The route's closure holds its `FusedHooks` and schemas, so a
|
|
3921
|
+
* request allocates no step closures.
|
|
3802
3922
|
*/
|
|
3803
3923
|
/**
|
|
3804
3924
|
* Keep the common one-derive/one-before shape bound to its hooks. Inline its synchronous path to
|
|
@@ -3910,8 +4030,10 @@ export class Server {
|
|
|
3910
4030
|
return this.respondFused(transformed, ctx, this.onResponseHooks.length === 0);
|
|
3911
4031
|
};
|
|
3912
4032
|
}
|
|
3913
|
-
buildFusedLifecycleWeb(route,
|
|
3914
|
-
|
|
4033
|
+
buildFusedLifecycleWeb(route, schema, maxBodyBytes) {
|
|
4034
|
+
const validationStages = compileFusedValidationStages(schema);
|
|
4035
|
+
const bodySchema = schema?.body;
|
|
4036
|
+
if (validationStages.length === 0 &&
|
|
3915
4037
|
bodySchema === undefined &&
|
|
3916
4038
|
route.errors.length === 0 &&
|
|
3917
4039
|
route.decorations === undefined &&
|
|
@@ -3926,48 +4048,28 @@ export class Server {
|
|
|
3926
4048
|
}
|
|
3927
4049
|
const runDerive = sharedStages(WEB_STAGES, this, route, (d, b, a) => this.fusedWebStages(d, b, a));
|
|
3928
4050
|
const decorations = route.decorations;
|
|
3929
|
-
if (bodySchema
|
|
3930
|
-
const onParsed = (parsed, ctx) => {
|
|
3931
|
-
let validation;
|
|
3932
|
-
try {
|
|
3933
|
-
validation = bodySchema["~standard"].validate(parsed);
|
|
3934
|
-
}
|
|
3935
|
-
catch (err) {
|
|
3936
|
-
return this.fusedWebRouteFail(err, ctx, route);
|
|
3937
|
-
}
|
|
3938
|
-
if (validation instanceof Promise) {
|
|
3939
|
-
return validation.then((settled) => {
|
|
3940
|
-
if (settled.issues !== undefined)
|
|
3941
|
-
return this.wrapWebResponse(plainValidationError(settled.issues));
|
|
3942
|
-
ctx.body = settled.value;
|
|
3943
|
-
return runDerive(ctx, route, 0);
|
|
3944
|
-
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3945
|
-
}
|
|
3946
|
-
if (validation.issues !== undefined)
|
|
3947
|
-
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
3948
|
-
ctx.body = validation.value;
|
|
3949
|
-
return runDerive(ctx, route, 0);
|
|
3950
|
-
};
|
|
4051
|
+
if (bodySchema === undefined) {
|
|
3951
4052
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
3952
4053
|
const ctx = nativeContext
|
|
3953
4054
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
3954
4055
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
3955
4056
|
if (decorations !== undefined)
|
|
3956
4057
|
Object.assign(ctx, decorations);
|
|
3957
|
-
|
|
4058
|
+
if (validationStages.length === 0)
|
|
4059
|
+
return runDerive(ctx, route, 0);
|
|
4060
|
+
return runFusedValidationStages(validationStages, source, ctx, this.wrapWebResponse, (err) => this.fusedWebRouteFail(err, ctx, route), () => runDerive(ctx, route, 0));
|
|
3958
4061
|
};
|
|
3959
4062
|
}
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
return runDerive(ctx, route, 0);
|
|
4063
|
+
const queryIndex = schema?.query === undefined ? validationStages.length : validationStages.length - 1;
|
|
4064
|
+
const beforeBodyStages = validationStages.slice(0, queryIndex);
|
|
4065
|
+
const afterBodyStages = validationStages.slice(queryIndex);
|
|
4066
|
+
const runAfterBody = (source, ctx) => afterBodyStages.length === 0
|
|
4067
|
+
? runDerive(ctx, route, 0)
|
|
4068
|
+
: runFusedValidationStages(afterBodyStages, source, ctx, this.wrapWebResponse, (err) => this.fusedWebRouteFail(err, ctx, route), () => runDerive(ctx, route, 0));
|
|
4069
|
+
const onParsed = (parsed, source, ctx) => {
|
|
3968
4070
|
let validation;
|
|
3969
4071
|
try {
|
|
3970
|
-
validation =
|
|
4072
|
+
validation = bodySchema["~standard"].validate(parsed);
|
|
3971
4073
|
}
|
|
3972
4074
|
catch (err) {
|
|
3973
4075
|
return this.fusedWebRouteFail(err, ctx, route);
|
|
@@ -3976,14 +4078,25 @@ export class Server {
|
|
|
3976
4078
|
return validation.then((settled) => {
|
|
3977
4079
|
if (settled.issues !== undefined)
|
|
3978
4080
|
return this.wrapWebResponse(plainValidationError(settled.issues));
|
|
3979
|
-
ctx.
|
|
3980
|
-
return
|
|
4081
|
+
ctx.body = settled.value;
|
|
4082
|
+
return runAfterBody(source, ctx);
|
|
3981
4083
|
}, (err) => this.fusedWebRouteFail(err, ctx, route));
|
|
3982
4084
|
}
|
|
3983
4085
|
if (validation.issues !== undefined)
|
|
3984
4086
|
return this.wrapWebResponse(plainValidationError(validation.issues));
|
|
3985
|
-
ctx.
|
|
3986
|
-
return
|
|
4087
|
+
ctx.body = validation.value;
|
|
4088
|
+
return runAfterBody(source, ctx);
|
|
4089
|
+
};
|
|
4090
|
+
const readBody = (source, ctx) => readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, source, ctx), this.wrapWebResponse, (err) => this.fusedWebRouteFail(err, ctx, route), bodySchema, this.formOrigins);
|
|
4091
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
4092
|
+
const ctx = nativeContext
|
|
4093
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
4094
|
+
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
4095
|
+
if (decorations !== undefined)
|
|
4096
|
+
Object.assign(ctx, decorations);
|
|
4097
|
+
if (beforeBodyStages.length === 0)
|
|
4098
|
+
return readBody(source, ctx);
|
|
4099
|
+
return runFusedValidationStages(beforeBodyStages, source, ctx, this.wrapWebResponse, (err) => this.fusedWebRouteFail(err, ctx, route), () => readBody(source, ctx));
|
|
3987
4100
|
};
|
|
3988
4101
|
}
|
|
3989
4102
|
/**
|
|
@@ -4196,70 +4309,60 @@ export class Server {
|
|
|
4196
4309
|
* terminal `finalize` throw (an unstringifiable handler return on the Node lane) is caught as
|
|
4197
4310
|
* `finishLifecycleContract` catches it; the body's 415/400 renders take `wrapResponse` directly.
|
|
4198
4311
|
*/
|
|
4199
|
-
buildFusedLifecycleRunner(route,
|
|
4312
|
+
buildFusedLifecycleRunner(route, schema, maxBodyBytes) {
|
|
4313
|
+
const validationStages = compileFusedValidationStages(schema);
|
|
4314
|
+
const bodySchema = schema?.body;
|
|
4200
4315
|
const runDerive = sharedStages(NODE_STAGES, this, route, (d, b, a) => this.fusedNodeStages(d, b, a));
|
|
4201
4316
|
const decorations = route.decorations;
|
|
4202
|
-
if (bodySchema
|
|
4203
|
-
const onParsed = (parsed, ctx, finalize, wrapResponse) => {
|
|
4204
|
-
let validation;
|
|
4205
|
-
try {
|
|
4206
|
-
validation = bodySchema["~standard"].validate(parsed);
|
|
4207
|
-
}
|
|
4208
|
-
catch (err) {
|
|
4209
|
-
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4210
|
-
}
|
|
4211
|
-
if (validation instanceof Promise) {
|
|
4212
|
-
return validation.then((settled) => {
|
|
4213
|
-
if (settled.issues !== undefined)
|
|
4214
|
-
return wrapResponse(plainValidationError(settled.issues));
|
|
4215
|
-
ctx.body = settled.value;
|
|
4216
|
-
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
4217
|
-
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
4218
|
-
}
|
|
4219
|
-
if (validation.issues !== undefined)
|
|
4220
|
-
return wrapResponse(plainValidationError(validation.issues));
|
|
4221
|
-
ctx.body = validation.value;
|
|
4222
|
-
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
4223
|
-
};
|
|
4317
|
+
if (bodySchema === undefined) {
|
|
4224
4318
|
return (source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse) => {
|
|
4225
4319
|
const ctx = nativeContext
|
|
4226
4320
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
4227
4321
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
4228
4322
|
if (decorations !== undefined)
|
|
4229
4323
|
Object.assign(ctx, decorations);
|
|
4230
|
-
|
|
4324
|
+
if (validationStages.length === 0)
|
|
4325
|
+
return runDerive(ctx, route, finalize, wrapResponse, 0);
|
|
4326
|
+
return runFusedValidationStages(validationStages, source, ctx, wrapResponse, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse), () => runDerive(ctx, route, finalize, wrapResponse, 0));
|
|
4231
4327
|
};
|
|
4232
4328
|
}
|
|
4233
|
-
const
|
|
4329
|
+
const queryIndex = schema?.query === undefined ? validationStages.length : validationStages.length - 1;
|
|
4330
|
+
const beforeBodyStages = validationStages.slice(0, queryIndex);
|
|
4331
|
+
const afterBodyStages = validationStages.slice(queryIndex);
|
|
4332
|
+
const runAfterBody = (source, ctx, finalize, wrapResponse) => afterBodyStages.length === 0
|
|
4333
|
+
? runDerive(ctx, route, finalize, wrapResponse, 0)
|
|
4334
|
+
: runFusedValidationStages(afterBodyStages, source, ctx, wrapResponse, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse), () => runDerive(ctx, route, finalize, wrapResponse, 0));
|
|
4335
|
+
const onParsed = (parsed, source, ctx, finalize, wrapResponse) => {
|
|
4336
|
+
let validation;
|
|
4337
|
+
try {
|
|
4338
|
+
validation = bodySchema["~standard"].validate(parsed);
|
|
4339
|
+
}
|
|
4340
|
+
catch (err) {
|
|
4341
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4342
|
+
}
|
|
4234
4343
|
if (validation instanceof Promise) {
|
|
4235
4344
|
return validation.then((settled) => {
|
|
4236
4345
|
if (settled.issues !== undefined)
|
|
4237
4346
|
return wrapResponse(plainValidationError(settled.issues));
|
|
4238
|
-
ctx.
|
|
4239
|
-
return
|
|
4347
|
+
ctx.body = settled.value;
|
|
4348
|
+
return runAfterBody(source, ctx, finalize, wrapResponse);
|
|
4240
4349
|
}, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse));
|
|
4241
4350
|
}
|
|
4242
4351
|
if (validation.issues !== undefined)
|
|
4243
4352
|
return wrapResponse(plainValidationError(validation.issues));
|
|
4244
|
-
ctx.
|
|
4245
|
-
return
|
|
4353
|
+
ctx.body = validation.value;
|
|
4354
|
+
return runAfterBody(source, ctx, finalize, wrapResponse);
|
|
4246
4355
|
};
|
|
4356
|
+
const readBody = (source, ctx, finalize, wrapResponse) => readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, source, ctx, finalize, wrapResponse), wrapResponse, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse), bodySchema, this.formOrigins);
|
|
4247
4357
|
return (source, params, search, signal, budget, platform, nativeContext, finalize, wrapResponse) => {
|
|
4248
4358
|
const ctx = nativeContext
|
|
4249
4359
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
4250
4360
|
: new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
|
|
4251
4361
|
if (decorations !== undefined)
|
|
4252
4362
|
Object.assign(ctx, decorations);
|
|
4253
|
-
if (
|
|
4254
|
-
return
|
|
4255
|
-
|
|
4256
|
-
try {
|
|
4257
|
-
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
|
|
4258
|
-
}
|
|
4259
|
-
catch (err) {
|
|
4260
|
-
return this.fusedFail(err, ctx, route, finalize, wrapResponse);
|
|
4261
|
-
}
|
|
4262
|
-
return runValidated(validation, ctx, finalize, wrapResponse);
|
|
4363
|
+
if (beforeBodyStages.length === 0)
|
|
4364
|
+
return readBody(source, ctx, finalize, wrapResponse);
|
|
4365
|
+
return runFusedValidationStages(beforeBodyStages, source, ctx, wrapResponse, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse), () => readBody(source, ctx, finalize, wrapResponse));
|
|
4263
4366
|
};
|
|
4264
4367
|
}
|
|
4265
4368
|
/** The thrown-value render of the Web fused lifecycle lanes for a route with no error hooks: a
|
|
@@ -4944,7 +5047,7 @@ export class Server {
|
|
|
4944
5047
|
* the table is kept to those paths. Where Bun would otherwise pass a request on to a less specific
|
|
4945
5048
|
* path, the more specific one carries `fallback`, the portable dispatcher, under that method.
|
|
4946
5049
|
* `peer` is the platform every request served from the table shares. */
|
|
4947
|
-
buildBunNativeRoutes(fallback, peer, platformFor) {
|
|
5050
|
+
buildBunNativeRoutes(fallback, peer, platformFor, allowStaticResponses = true) {
|
|
4948
5051
|
const hooked = this.onRequestHooks.length > 0 && !this.bunNativeRequestHooksSafe;
|
|
4949
5052
|
if ((hooked && platformFor === undefined) ||
|
|
4950
5053
|
// A hook may rewrite a handshake onto a socket path, which only the dispatcher's upgrade check follows.
|
|
@@ -4960,6 +5063,18 @@ export class Server {
|
|
|
4960
5063
|
// The capacity gate must wrap every request; the fused lane bypasses fetchMatched, so enabling
|
|
4961
5064
|
// admission drops fusion (native matching stays) and routes through the gated matched lane.
|
|
4962
5065
|
this.capacityGate === undefined;
|
|
5066
|
+
// A response value skips every request callback, so every policy that could inspect it must be absent.
|
|
5067
|
+
const mayUseStaticResponse = allowStaticResponses &&
|
|
5068
|
+
!this.validateHost &&
|
|
5069
|
+
!this.normalizeTarget &&
|
|
5070
|
+
this.requestTimeoutMs === 0 &&
|
|
5071
|
+
!this.acceptInboundDeadlines &&
|
|
5072
|
+
this.capacityGate === undefined &&
|
|
5073
|
+
this.onRequestHooks.length === 0 &&
|
|
5074
|
+
this.onResponseHooks.length === 0 &&
|
|
5075
|
+
this.onResponseFinalizedHooks.length === 0 &&
|
|
5076
|
+
this.staticResponseHeaders === undefined &&
|
|
5077
|
+
this.responseBodyTag === undefined;
|
|
4963
5078
|
const unboundedSignal = mayUseFusedNative ? getNeverAbortSignal() : undefined;
|
|
4964
5079
|
const unboundedBudget = mayUseFusedNative ? getUnboundedRequestBudget() : undefined;
|
|
4965
5080
|
let count = 0;
|
|
@@ -5021,7 +5136,8 @@ export class Server {
|
|
|
5021
5136
|
return routes[key];
|
|
5022
5137
|
};
|
|
5023
5138
|
for (let i = 0; i < all.length; i++) {
|
|
5024
|
-
const
|
|
5139
|
+
const route = all[i];
|
|
5140
|
+
const { method, path, pattern, entry } = route;
|
|
5025
5141
|
const run = lead[i];
|
|
5026
5142
|
// The route is served from the table unless its path cannot be there, Bun's table has no slot
|
|
5027
5143
|
// for its method, or a path outside the table outranks it: Bun would hand this route a request
|
|
@@ -5036,6 +5152,32 @@ export class Server {
|
|
|
5036
5152
|
(method === "GET" && sockets.some((socket) => mayShareRequest(socket, pattern.segments)))) {
|
|
5037
5153
|
continue;
|
|
5038
5154
|
}
|
|
5155
|
+
const staticResponse = entry.staticResponse;
|
|
5156
|
+
if (mayUseStaticResponse &&
|
|
5157
|
+
staticResponse !== undefined &&
|
|
5158
|
+
(method === "GET" || method === "HEAD") &&
|
|
5159
|
+
// Bun 1.3 can misframe null-body status codes in its native static table.
|
|
5160
|
+
staticResponse.status !== 204 &&
|
|
5161
|
+
staticResponse.status !== 205 &&
|
|
5162
|
+
staticResponse.status !== 304 &&
|
|
5163
|
+
pattern.paramNames.length === 0 &&
|
|
5164
|
+
entry.schema === undefined &&
|
|
5165
|
+
entry.idempotent === undefined &&
|
|
5166
|
+
entry.ledgered === undefined &&
|
|
5167
|
+
entry.responseContract === undefined &&
|
|
5168
|
+
entry.derives.length === 0 &&
|
|
5169
|
+
entry.authStages.length === 0 &&
|
|
5170
|
+
!entry.hasDecorations &&
|
|
5171
|
+
entry.beforeHandle.length === 0 &&
|
|
5172
|
+
entry.afterHandle.length === 0 &&
|
|
5173
|
+
entry.onError.length === 0 &&
|
|
5174
|
+
entry.around.length === 0 &&
|
|
5175
|
+
route.assurance.length === 0 &&
|
|
5176
|
+
(route.descriptor.capabilities?.length ?? 0) === 0) {
|
|
5177
|
+
tableAt(path, pattern.segments)[method] = createStaticResponse(staticResponse);
|
|
5178
|
+
count += 1;
|
|
5179
|
+
continue;
|
|
5180
|
+
}
|
|
5039
5181
|
const paramNames = pattern.paramNames;
|
|
5040
5182
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined;
|
|
5041
5183
|
const handler = this.compileBunNativeHandler(entry, method, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback, hooked ? platformFor : undefined);
|
|
@@ -5133,8 +5275,8 @@ export class Server {
|
|
|
5133
5275
|
// matched route's handler, set by `server.upgrade`.
|
|
5134
5276
|
// With WS routes, the dispatcher comes from the installed `.use(websocket())` runtime - non-null
|
|
5135
5277
|
// because a socket route means ws() ran, and ws() requires the runtime at registration.
|
|
5136
|
-
// Native pub/sub when the app has WS routes and none validate outbound
|
|
5137
|
-
// `app.publish` go through Bun's own (uWebSockets) broadcast
|
|
5278
|
+
// Native pub/sub when the app has WS routes, no custom adapter, and none validate outbound
|
|
5279
|
+
// frames: `ws.subscribe` and `app.publish` go through Bun's own (uWebSockets) broadcast.
|
|
5138
5280
|
// A mounted app counts only while it has a WebSocket route (see `[GET_WS_RUNTIME]`), so composing
|
|
5139
5281
|
// one without any needs no runtime here.
|
|
5140
5282
|
const wsRuntime = this[GET_WS_RUNTIME]();
|
|
@@ -5142,10 +5284,13 @@ export class Server {
|
|
|
5142
5284
|
throw new FrameworkError("INVALID_WS_RUNTIME", "a Bun server with mounted WebSocket routes needs the websocket() runtime installed on the parent or child app");
|
|
5143
5285
|
}
|
|
5144
5286
|
// Native pub/sub is safe only when every upgrade belongs to this app. A mounted child that takes
|
|
5145
|
-
// upgrades owns a different
|
|
5146
|
-
// that
|
|
5287
|
+
// upgrades owns a different topic adapter, so force the per-connection JS dispatcher and carry
|
|
5288
|
+
// that adapter in the upgrade outcome instead of accidentally broadcasting child sockets through
|
|
5147
5289
|
// the parent.
|
|
5148
|
-
const nativePubsub = this.wsRoutes.length > 0 &&
|
|
5290
|
+
const nativePubsub = this.wsRoutes.length > 0 &&
|
|
5291
|
+
this.options.topicAdapter === undefined &&
|
|
5292
|
+
!this.mountsTakeUpgrades() &&
|
|
5293
|
+
!this.wsHasValidatedSend;
|
|
5149
5294
|
const wsHandlers = wsRuntime?.bunHandlers(this.topics ?? wsRuntime.createTopics(), nativePubsub, this);
|
|
5150
5295
|
// Spread rather than pass `hostname: undefined` (or an undefined idle timeout or TLS) - Bun treats
|
|
5151
5296
|
// an explicit undefined as a value on some option paths, and omitting is what selects its own
|
|
@@ -5192,7 +5337,7 @@ export class Server {
|
|
|
5192
5337
|
};
|
|
5193
5338
|
const nativeRoutes = this.buildBunNativeRoutes(fallback, peer, (request, server) =>
|
|
5194
5339
|
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: `fallback` hands `fetch` the same platform, which holds no environment.
|
|
5195
|
-
new BunPeerPlatform(server, request, setIdleTimeout));
|
|
5340
|
+
new BunPeerPlatform(server, request, setIdleTimeout), options?.static === undefined);
|
|
5196
5341
|
const first = options?.static;
|
|
5197
5342
|
const before = (inner) => first === undefined
|
|
5198
5343
|
? inner
|
|
@@ -5210,7 +5355,7 @@ export class Server {
|
|
|
5210
5355
|
const methods = nativeRoutes[path];
|
|
5211
5356
|
for (const method of BUN_TABLE_METHODS) {
|
|
5212
5357
|
const handler = methods[method];
|
|
5213
|
-
if (handler
|
|
5358
|
+
if (typeof handler === "function")
|
|
5214
5359
|
methods[method] = before(handler);
|
|
5215
5360
|
}
|
|
5216
5361
|
}
|