@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/edge-kit.d.ts +1 -1
  3. package/dist/edge-kit.d.ts.map +1 -1
  4. package/dist/edge-kit.js +1 -1
  5. package/dist/edge-kit.js.map +1 -1
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal/capability-runtime.d.ts.map +1 -1
  11. package/dist/internal/capability-runtime.js +3 -2
  12. package/dist/internal/capability-runtime.js.map +1 -1
  13. package/dist/internal/route-execution.d.ts +6 -4
  14. package/dist/internal/route-execution.d.ts.map +1 -1
  15. package/dist/internal/route-execution.js.map +1 -1
  16. package/dist/internal/route-lanes.d.ts +4 -4
  17. package/dist/internal/route-lanes.d.ts.map +1 -1
  18. package/dist/internal/route-lanes.js +37 -37
  19. package/dist/internal/route-lanes.js.map +1 -1
  20. package/dist/manifest.d.ts.map +1 -1
  21. package/dist/manifest.js +2 -1
  22. package/dist/manifest.js.map +1 -1
  23. package/dist/server/body.d.ts +2 -0
  24. package/dist/server/body.d.ts.map +1 -1
  25. package/dist/server/body.js +2 -0
  26. package/dist/server/body.js.map +1 -1
  27. package/dist/server/core-copies.js +1 -1
  28. package/dist/server/server-types.d.ts +7 -1
  29. package/dist/server/server-types.d.ts.map +1 -1
  30. package/dist/server/server.d.ts +16 -24
  31. package/dist/server/server.d.ts.map +1 -1
  32. package/dist/server/server.js +285 -140
  33. package/dist/server/server.js.map +1 -1
  34. package/dist/server/static-response.d.ts +13 -0
  35. package/dist/server/static-response.d.ts.map +1 -0
  36. package/dist/server/static-response.js +28 -0
  37. package/dist/server/static-response.js.map +1 -0
  38. package/dist/server/websocket.d.ts +23 -7
  39. package/dist/server/websocket.d.ts.map +1 -1
  40. package/dist/server/websocket.js +8 -3
  41. package/dist/server/websocket.js.map +1 -1
  42. package/dist/server/ws-adapter-view.d.ts +3 -3
  43. package/dist/server/ws-adapter-view.d.ts.map +1 -1
  44. package/dist/server/ws-bun.d.ts +4 -4
  45. package/dist/server/ws-bun.d.ts.map +1 -1
  46. package/dist/server/ws-bun.js +12 -5
  47. package/dist/server/ws-bun.js.map +1 -1
  48. package/dist/server/ws-hook.d.ts +4 -4
  49. package/dist/server/ws-hook.d.ts.map +1 -1
  50. package/dist/server.d.ts +2 -1
  51. package/dist/server.d.ts.map +1 -1
  52. package/dist/server.js +1 -0
  53. package/dist/server.js.map +1 -1
  54. package/dist/tool-contract.d.ts +5 -0
  55. package/dist/tool-contract.d.ts.map +1 -1
  56. package/dist/tool-contract.js +6 -3
  57. package/dist/tool-contract.js.map +1 -1
  58. package/dist/ws.d.ts +1 -1
  59. package/dist/ws.d.ts.map +1 -1
  60. package/dist/ws.js.map +1 -1
  61. package/package.json +1 -1
  62. package/src/edge-kit.ts +1 -1
  63. package/src/index.ts +3 -1
  64. package/src/internal/capability-runtime.ts +3 -2
  65. package/src/internal/route-execution.ts +6 -4
  66. package/src/internal/route-lanes.ts +44 -44
  67. package/src/manifest.ts +2 -1
  68. package/src/server/body.ts +3 -0
  69. package/src/server/core-copies.ts +1 -1
  70. package/src/server/server-types.ts +7 -1
  71. package/src/server/server.ts +414 -189
  72. package/src/server/static-response.ts +36 -0
  73. package/src/server/websocket.ts +29 -8
  74. package/src/server/ws-adapter-view.ts +3 -3
  75. package/src/server/ws-bun.ts +15 -9
  76. package/src/server/ws-hook.ts +4 -3
  77. package/src/server.ts +2 -0
  78. package/src/tool-contract.ts +13 -3
  79. package/src/ws.ts +1 -0
@@ -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
- /** In-process pub/sub backing `ws.subscribe(topic)` + `app.publish(topic, data)` (single-instance).
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 registry loop
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 validated-send route,
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 = undefined;
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 registry loop that runs each send through the sender.
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
- * In-process and **single-instance** (see {@link TopicRegistry}) - a multi-instance deploy must bridge
1602
- * an external fan-out (Redis, a Durable Object) to this. A no-op when nobody is subscribed.
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 registry runs.
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
- // No `app.ws()` yet ⇒ no registry and necessarily no subscribers - a publish is a no-op anyway.
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
- // `selectRouteLanes` admits the fused hook lanes only for derive, before and after hooks, so the
1712
- // three arrays below are the route's whole lifecycle.
1713
- const fusedLifecycle = lanes.fusedLane === "hooks" || lanes.fusedLane === "body-hooks";
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?.query, schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
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?.query, schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
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" || laneName === "body-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?.query, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
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?.query, entry.schema?.body, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
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 + registry.
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-only route's parser → validator → handler continuation once. The
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
- const finish = (value) => runParsed(value, ctx, finalize, wrapResponse);
3537
- return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, finish, wrapResponse, (err) => logError(err, ctx, finalize, wrapResponse), bodySchema, this.formOrigins);
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 a route whose only lifecycle step is a query schema: parse + validate +
3546
- * handler + respond in one closure, no lifecycle promise when the validator and handler are sync.
3547
- * Eligibility is decided at registration (see `fusedQuery` in {@link register}); the semantics here
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
- ctx.query = value;
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 = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
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) => settled.issues !== undefined
3586
- ? this.wrapWebResponse(plainValidationError(settled.issues))
3587
- : runHandler(ctx, settled.value), (err) => logError(err, ctx));
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
- return runHandler(ctx, validation.value);
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 only
3796
- * derive, before and after hooks - no params or headers schema, onError, around, decorations,
3797
- * response contract or recovery hook, and never a query schema beside a body schema - so the behavior
3798
- * is the compiled program's: the same stage order, the same thrown-value contract (`Response` is
3799
- * control flow, anything else logs and 500s), the same `c.set` semantics and `fusedRespond` render.
3800
- * The route's own closure holds only its `FusedHooks` and schemas and takes the request's context as
3801
- * an argument, so a request allocates no step closures.
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, querySchema, bodySchema, maxBodyBytes) {
3914
- if (querySchema === undefined &&
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 !== undefined) {
3930
- const onParsed = (parsed, ctx) => {
3931
- let validation;
3932
- try {
3933
- validation = bodySchema["~standard"].validate(parsed);
3934
- }
3935
- catch (err) {
3936
- return this.fusedWebRouteFail(err, ctx, route);
3937
- }
3938
- if (validation instanceof Promise) {
3939
- return validation.then((settled) => {
3940
- if (settled.issues !== undefined)
3941
- return this.wrapWebResponse(plainValidationError(settled.issues));
3942
- ctx.body = settled.value;
3943
- return runDerive(ctx, route, 0);
3944
- }, (err) => this.fusedWebRouteFail(err, ctx, route));
3945
- }
3946
- if (validation.issues !== undefined)
3947
- return this.wrapWebResponse(plainValidationError(validation.issues));
3948
- ctx.body = validation.value;
3949
- return runDerive(ctx, route, 0);
3950
- };
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
- return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, ctx), this.wrapWebResponse, (err) => this.fusedWebRouteFail(err, ctx, route), bodySchema, this.formOrigins);
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
- return (source, params, search, signal, budget, platform, nativeContext) => {
3961
- const ctx = nativeContext
3962
- ? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
3963
- : new RequestContext(source, params, search, signal, budget, platform, maxBodyBytes, this.protoPoisoning);
3964
- if (decorations !== undefined)
3965
- Object.assign(ctx, decorations);
3966
- if (querySchema === undefined)
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 = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]));
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.query = settled.value;
3980
- return runDerive(ctx, route, 0);
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.query = validation.value;
3986
- return runDerive(ctx, route, 0);
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, querySchema, bodySchema, maxBodyBytes) {
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 !== undefined) {
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
- return readBodyFramed(source, maxBodyBytes, this.protoPoisoning, (parsed) => onParsed(parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => this.fusedFail(err, ctx, route, finalize, wrapResponse), bodySchema, this.formOrigins);
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 runValidated = (validation, ctx, finalize, wrapResponse) => {
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.query = settled.value;
4239
- return runDerive(ctx, route, finalize, wrapResponse, 0);
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.query = validation.value;
4245
- return runDerive(ctx, route, finalize, wrapResponse, 0);
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 (querySchema === undefined)
4254
- return runDerive(ctx, route, finalize, wrapResponse, 0);
4255
- let validation;
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 { method, path, pattern, entry } = all[i];
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 frames: `ws.subscribe` and
5137
- // `app.publish` go through Bun's own (uWebSockets) broadcast instead of the JS registry loop.
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 TopicRegistry, so force the per-connection JS dispatcher and carry
5146
- // that registry in the upgrade outcome instead of accidentally broadcasting child sockets through
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 && !this.mountsTakeUpgrades() && !this.wsHasValidatedSend;
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 !== undefined)
5358
+ if (typeof handler === "function")
5214
5359
  methods[method] = before(handler);
5215
5360
  }
5216
5361
  }