@nifrajs/core 5.6.0 → 5.8.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 +99 -0
- package/README.md +5 -0
- package/dist/binary.d.ts +15 -1
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +1 -0
- package/dist/binary.js.map +1 -1
- package/dist/edge-kit.d.ts +1 -0
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +1 -0
- package/dist/edge-kit.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +2 -0
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/hook-origins.d.ts +2 -0
- package/dist/internal/hook-origins.d.ts.map +1 -1
- package/dist/internal/hook-origins.js +12 -0
- package/dist/internal/hook-origins.js.map +1 -1
- package/dist/reflection.d.ts +5 -0
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +12 -0
- package/dist/reflection.js.map +1 -1
- package/dist/server/answer-step.d.ts +27 -0
- package/dist/server/answer-step.d.ts.map +1 -0
- package/dist/server/answer-step.js +33 -0
- package/dist/server/answer-step.js.map +1 -0
- package/dist/server/base64-answers.d.ts +5 -0
- package/dist/server/base64-answers.d.ts.map +1 -0
- package/dist/server/base64-answers.js +132 -0
- package/dist/server/base64-answers.js.map +1 -0
- package/dist/server/core-copies.d.ts.map +1 -1
- package/dist/server/core-copies.js +5 -2
- package/dist/server/core-copies.js.map +1 -1
- package/dist/server/install.d.ts +2 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +2 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/json-response.d.ts +3 -0
- package/dist/server/json-response.d.ts.map +1 -0
- package/dist/server/json-response.js +14 -0
- package/dist/server/json-response.js.map +1 -0
- package/dist/server/json-text.d.ts +16 -0
- package/dist/server/json-text.d.ts.map +1 -0
- package/dist/server/json-text.js +34 -0
- package/dist/server/json-text.js.map +1 -0
- package/dist/server/node-outcome.d.ts +5 -0
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +3 -4
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +4 -2
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +2 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +27 -24
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +2 -1
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/route-template.d.ts +10 -0
- package/dist/server/route-template.d.ts.map +1 -0
- package/dist/server/route-template.js +21 -0
- package/dist/server/route-template.js.map +1 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +2 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +26 -6
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +22 -10
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +109 -45
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +2 -1
- package/dist/server/sse.js.map +1 -1
- package/dist/server/text-slots.d.ts +8 -0
- package/dist/server/text-slots.d.ts.map +1 -0
- package/dist/server/text-slots.js +19 -0
- package/dist/server/text-slots.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts +5 -0
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +26 -17
- package/dist/single-copy.js.map +1 -1
- package/dist/transport-codec.d.ts.map +1 -1
- package/dist/transport-codec.js +2 -1
- package/dist/transport-codec.js.map +1 -1
- package/package.json +1 -1
- package/src/binary.ts +17 -0
- package/src/edge-kit.ts +1 -0
- package/src/errors.ts +1 -0
- package/src/evidence.ts +4 -0
- package/src/index.ts +2 -1
- package/src/internal/hook-origins.ts +10 -0
- package/src/reflection.ts +18 -0
- package/src/server/answer-step.ts +68 -0
- package/src/server/base64-answers.ts +138 -0
- package/src/server/core-copies.ts +5 -2
- package/src/server/install.ts +3 -0
- package/src/server/json-response.ts +13 -0
- package/src/server/json-text.ts +34 -0
- package/src/server/node-outcome.ts +4 -5
- package/src/server/request-context.ts +4 -2
- package/src/server/respond.ts +27 -18
- package/src/server/response-contract-lane.ts +2 -1
- package/src/server/route-template.ts +24 -0
- package/src/server/runtime-core.ts +2 -1
- package/src/server/server-types.ts +27 -4
- package/src/server/server.ts +131 -63
- package/src/server/sse.ts +2 -1
- package/src/server/text-slots.ts +21 -0
- package/src/server.ts +1 -0
- package/src/single-copy.ts +31 -15
- package/src/transport-codec.ts +3 -1
package/dist/server/server.js
CHANGED
|
@@ -10,6 +10,8 @@ import { authenticateMount, compileRouteProgram, executeRouteProgram, } from "..
|
|
|
10
10
|
import { isSameOriginRequest } from "../internal/same-origin.js";
|
|
11
11
|
import { decodeRouteParams, expandOptionalParams, } from "../router/pattern.js";
|
|
12
12
|
import { EMPTY_PARAMS, METHODS, Router } from "../router/router.js";
|
|
13
|
+
import { withAnswerSteps } from "./answer-step.js";
|
|
14
|
+
import { isAuthenticatedRequest, markAuthenticatedRequest, } from "./auth.js";
|
|
13
15
|
import { emitRequestErrorLog, renderBareError } from "./bare-error-lane.js";
|
|
14
16
|
import { LAST_BEFORE_HANDLE } from "./before-handler.js";
|
|
15
17
|
import { assertByteLimit, clientBodyErrorCode, hasTrustedBodyFraming, markTransportCap, markTrustedBodyFraming, UNLIMITED_BODY_BYTES, } from "./body.js";
|
|
@@ -26,9 +28,10 @@ import { needsResolving, resolveDotSegments } from "./request-target.js";
|
|
|
26
28
|
import { applyStaticResponseHeaders, buildStaticResponseHeaders, claimHandedOver, fusedRespond, fusedRespondNoSet, handedBackResponse, recordResidentTexts, toResponse, writableResponse, } from "./respond.js";
|
|
27
29
|
import { CONTEXT_REQUEST, CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, } from "./runtime-core.js";
|
|
28
30
|
import { normalizeStaticResponseHeaders } from "./static-headers.js";
|
|
31
|
+
import { withTextSlots } from "./text-slots.js";
|
|
29
32
|
import { plainValidationError } from "./validation.js";
|
|
30
33
|
import { NIFRA_BACKEND_WS_RUNTIME } from "../mount.js";
|
|
31
|
-
import { GET_WS_RUNTIME, INSTALL_EFFECT_LEDGER, INSTALL_IDEMPOTENCY, INSTALL_MCP, INSTALL_NODE_DIRECT, INSTALL_NOT_FOUND, INSTALL_RESPONSE_CONTRACT, INSTALL_RESPONSE_OBSERVER, INSTALL_SSE, INSTALL_WS, NODE_NATIVE_MOUNT, REPORT_ERROR, RESOLVE_NODE_MOUNT, } from "./install.js";
|
|
34
|
+
import { GET_WS_RUNTIME, INSTALL_EFFECT_LEDGER, INSTALL_IDEMPOTENCY, INSTALL_MCP, INSTALL_NODE_DIRECT, INSTALL_NOT_FOUND, INSTALL_RESPONSE_CONTRACT, INSTALL_RESPONSE_OBSERVER, INSTALL_SSE, INSTALL_WS, NODE_NATIVE_MOUNT, REPORT_ERROR, RESOLVE_NODE_MOUNT, SERVER_LIFECYCLE, } from "./install.js";
|
|
32
35
|
import { jsonLogger } from "./logger.js";
|
|
33
36
|
// CORS uses this marker to say its request hook only handles OPTIONS preflight. Bun can keep
|
|
34
37
|
// ordinary registered routes on its native table and send unmatched OPTIONS requests through the
|
|
@@ -229,7 +232,8 @@ function resolvedSource(req, url) {
|
|
|
229
232
|
// request for `/public` into one for `/admin`. Node refuses such a Host before routing; so does this.
|
|
230
233
|
const HOST_AUTHORITY = /^(?:\[[0-9a-f:.%]+\]|[\w.-]+)(?::(\d{1,5}))?$/i;
|
|
231
234
|
// Clients repeat one Host on every request; comparing to the last valid one is ~7x cheaper than the regex.
|
|
232
|
-
|
|
235
|
+
// `null` until one passes, so an empty Host is never taken as the cached one.
|
|
236
|
+
let lastValidHost = null;
|
|
233
237
|
// A port past 65535, or an IPv6 literal no URL parser takes (`[1:2]`), is refused as Node refuses it:
|
|
234
238
|
// no URL can be built on either, so resolving a dot segment against one threw out of `fetch`.
|
|
235
239
|
const invalidHost = (host) => {
|
|
@@ -251,10 +255,10 @@ const invalidHost = (host) => {
|
|
|
251
255
|
* its dot segments are resolved; so does Bun when HTTP/1.0 leaves the Host out, and its route table
|
|
252
256
|
* matched `/files/..` raw. Such a request is served as `localhost`, as Node and Deno serve it.
|
|
253
257
|
* `headerOf`, not `req.headers`: on Deno that reads the Host alone instead of building every
|
|
254
|
-
* header.
|
|
258
|
+
* header. On Bun only a target that needs resolving is checked unless `validateHost` is set.
|
|
255
259
|
*/
|
|
256
|
-
const requestTarget = (req) => {
|
|
257
|
-
if (!rawRequestTargets && !unparsedBunTargets)
|
|
260
|
+
const requestTarget = (req, validateHost) => {
|
|
261
|
+
if (!rawRequestTargets && !(unparsedBunTargets && (validateHost || needsResolving(req.url))))
|
|
258
262
|
return false;
|
|
259
263
|
const host = headerOf(req, "host");
|
|
260
264
|
if (host !== null && invalidHost(host)) {
|
|
@@ -303,13 +307,17 @@ function mountPriorityOf(priority) {
|
|
|
303
307
|
function compareMounts(a, b) {
|
|
304
308
|
return b.priority - a.priority || b.path.length - a.path.length || a.order - b.order;
|
|
305
309
|
}
|
|
310
|
+
/** Rebase a mounted request's path without losing the authentication mark used by child caches. */
|
|
306
311
|
function stripMountPrefix(request, prefix) {
|
|
307
312
|
if (prefix === "/")
|
|
308
313
|
return request;
|
|
309
314
|
const url = new URL(request.url);
|
|
310
315
|
const rest = url.pathname.slice(prefix.length);
|
|
311
316
|
url.pathname = rest === "" ? "/" : rest;
|
|
312
|
-
|
|
317
|
+
const childRequest = new Request(url.href, request);
|
|
318
|
+
if (isAuthenticatedRequest(request))
|
|
319
|
+
markAuthenticatedRequest(childRequest);
|
|
320
|
+
return childRequest;
|
|
313
321
|
}
|
|
314
322
|
function responseSet(ctx) {
|
|
315
323
|
return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS;
|
|
@@ -646,6 +654,7 @@ export class Server {
|
|
|
646
654
|
formOrigins;
|
|
647
655
|
/** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
|
|
648
656
|
trustBodyFraming;
|
|
657
|
+
validateHost;
|
|
649
658
|
wsMaxPayloadBytes;
|
|
650
659
|
requestTimeoutMs;
|
|
651
660
|
wsUpgradeTimeoutMs;
|
|
@@ -659,6 +668,7 @@ export class Server {
|
|
|
659
668
|
deadlineAdmissionOptions;
|
|
660
669
|
gracefulSignals;
|
|
661
670
|
stopHooks;
|
|
671
|
+
stoppingHooks;
|
|
662
672
|
fetchMounts;
|
|
663
673
|
mountOrder;
|
|
664
674
|
/** Number of composed mounts that must run before the parent route table. Keeps legacy mounts off the hot path. */
|
|
@@ -742,7 +752,7 @@ export class Server {
|
|
|
742
752
|
? fusedRespondNoSet(result, this.responseBodyTag, this.staticResponseHeaders, true)
|
|
743
753
|
: toResponse(result, set, this.responseBodyTag, this.staticResponseHeaders,
|
|
744
754
|
// Response hooks write next, and they run for this same request object.
|
|
745
|
-
ctx !== undefined && this.onResponseHooks.length > 0 ? ctx[CONTEXT_REQUEST] : undefined);
|
|
755
|
+
ctx !== undefined && this.onResponseHooks.length > 0 ? ctx[CONTEXT_REQUEST] : undefined, this.onResponseHooks.length === 0);
|
|
746
756
|
responseRequests;
|
|
747
757
|
/** A request the WebSocket handshake check ran the onRequest hooks on and then passed to HTTP →
|
|
748
758
|
* what the hooks returned, so `fetch` routes that instead of running the hooks a second time. */
|
|
@@ -777,6 +787,7 @@ export class Server {
|
|
|
777
787
|
this.protoPoisoning = options.protoPoisoning ?? "reject";
|
|
778
788
|
this.formOrigins = options.formOrigins;
|
|
779
789
|
this.trustBodyFraming = options.trustBodyFraming ?? false;
|
|
790
|
+
this.validateHost = options.validateHost ?? false;
|
|
780
791
|
const wsMaxPayloadBytes = options.wsMaxPayloadBytes ?? maxBodyBytes;
|
|
781
792
|
assertByteLimit(wsMaxPayloadBytes, "wsMaxPayloadBytes");
|
|
782
793
|
this.maxBodyBytes = maxBodyBytes;
|
|
@@ -808,6 +819,7 @@ export class Server {
|
|
|
808
819
|
this.gracefulSignals =
|
|
809
820
|
options.gracefulSignals === true ? {} : options.gracefulSignals || undefined;
|
|
810
821
|
this.stopHooks = [];
|
|
822
|
+
this.stoppingHooks = [];
|
|
811
823
|
this.fetchMounts = [];
|
|
812
824
|
this.mountOrder = 0;
|
|
813
825
|
this.preRouteMountCount = 0;
|
|
@@ -878,6 +890,18 @@ export class Server {
|
|
|
878
890
|
this.stopHooks.push(fn);
|
|
879
891
|
return this;
|
|
880
892
|
}
|
|
893
|
+
/**
|
|
894
|
+
* Register a callback awaited as soon as `stop()` begins, while the server still serves: readiness
|
|
895
|
+
* should fail from here, and work that outlives a request (a poller, a queue consumer) should stop
|
|
896
|
+
* taking more. Hung callbacks are bounded.
|
|
897
|
+
*/
|
|
898
|
+
onStopping(fn) {
|
|
899
|
+
this.assertConfigurable("onStopping()");
|
|
900
|
+
if (typeof fn !== "function")
|
|
901
|
+
throw new TypeError("onStopping callback must be a function");
|
|
902
|
+
this.stoppingHooks.push(fn);
|
|
903
|
+
return this;
|
|
904
|
+
}
|
|
881
905
|
/**
|
|
882
906
|
* Mount a legacy fetch handler under a literal path prefix. Mounted handlers sit behind typed
|
|
883
907
|
* routes, so routes can move one at a time while the remaining legacy surface stays live.
|
|
@@ -1575,6 +1599,7 @@ export class Server {
|
|
|
1575
1599
|
throw new RouteConfigError("INVALID_PATH", `group(${JSON.stringify(prefix)}) route path must start with "/": ${JSON.stringify(path)}`);
|
|
1576
1600
|
}
|
|
1577
1601
|
prepareRoute(method, path, schema, handler) {
|
|
1602
|
+
schema = withTextSlots(schema);
|
|
1578
1603
|
// Route provenance for the CLI's capability walk, which sets NIFRA_ROUTE_SITES while it loads the
|
|
1579
1604
|
// app: the module that registered the route, read where a static scan cannot see the path.
|
|
1580
1605
|
if (hookAuditRuntime &&
|
|
@@ -1604,6 +1629,15 @@ export class Server {
|
|
|
1604
1629
|
defaultOnValidationError: this.defaultOnValidationError,
|
|
1605
1630
|
}, method, path, schema, handler);
|
|
1606
1631
|
const { pattern, bodyLimit, capabilities, routeDecorations, hasDecorations, idempotent, ledgered, responseContract, lanes, routeAssurance, } = compiled;
|
|
1632
|
+
// A schema that writes its answer its own way (`t.bytes()`) wraps every stage that can answer.
|
|
1633
|
+
const stages = withAnswerSteps(schema, method, path, {
|
|
1634
|
+
handler,
|
|
1635
|
+
beforeHandle: this.beforeHandleHooks,
|
|
1636
|
+
afterHandle: this.afterHandleHooks,
|
|
1637
|
+
onError: this.onErrorHooks,
|
|
1638
|
+
});
|
|
1639
|
+
handler = stages.handler;
|
|
1640
|
+
const { beforeHandle, afterHandle, onError } = stages;
|
|
1607
1641
|
if (idempotent !== undefined && this.idempotencyRuntime !== undefined) {
|
|
1608
1642
|
handler = this.idempotencyRuntime.wrapHandler(handler);
|
|
1609
1643
|
}
|
|
@@ -1622,11 +1656,11 @@ export class Server {
|
|
|
1622
1656
|
const fusedBodyRunner = fusedBody
|
|
1623
1657
|
? this.buildFusedBodyRunner(handler, schema?.body, hasDecorations ? routeDecorations : undefined, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1624
1658
|
: isFusedBodyLifecycle
|
|
1625
|
-
? this.buildFusedBodyDeriveBeforeAfterRunner(handler, this.derives[0],
|
|
1659
|
+
? this.buildFusedBodyDeriveBeforeAfterRunner(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1626
1660
|
: isFusedLifecycle && lifecycleHookLane === "derive-before"
|
|
1627
|
-
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0],
|
|
1661
|
+
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0], beforeHandle[0], undefined, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1628
1662
|
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
1629
|
-
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0],
|
|
1663
|
+
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1630
1664
|
: undefined;
|
|
1631
1665
|
const fusedWeb = bare && this.aroundHooks.length === 0
|
|
1632
1666
|
? this.buildFusedWeb(handler, hasDecorations ? routeDecorations : undefined, isContextlessNoArgArrow(handler), bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
@@ -1635,20 +1669,20 @@ export class Server {
|
|
|
1635
1669
|
: fusedBody
|
|
1636
1670
|
? this.buildFusedBodyWeb(fusedBodyRunner)
|
|
1637
1671
|
: isFusedBodyLifecycle
|
|
1638
|
-
? this.buildFusedBodyDeriveBeforeAfter(handler, this.derives[0],
|
|
1672
|
+
? this.buildFusedBodyDeriveBeforeAfter(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1639
1673
|
: isFusedLifecycle && lifecycleHookLane === "derive-before"
|
|
1640
|
-
? this.buildFusedDeriveBefore(handler, this.derives[0],
|
|
1674
|
+
? this.buildFusedDeriveBefore(handler, this.derives[0], beforeHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1641
1675
|
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
1642
|
-
? this.buildFusedDeriveBeforeAfter(handler, this.derives[0],
|
|
1676
|
+
? this.buildFusedDeriveBeforeAfter(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1643
1677
|
: undefined;
|
|
1644
1678
|
const program = compileRouteProgram({
|
|
1645
1679
|
schema,
|
|
1646
1680
|
handler: handler,
|
|
1647
1681
|
derives: this.derives,
|
|
1648
1682
|
authStages: compiled.authStages,
|
|
1649
|
-
beforeHandle
|
|
1650
|
-
afterHandle
|
|
1651
|
-
onError
|
|
1683
|
+
beforeHandle,
|
|
1684
|
+
afterHandle,
|
|
1685
|
+
onError,
|
|
1652
1686
|
decorations: routeDecorations,
|
|
1653
1687
|
hasDecorations,
|
|
1654
1688
|
bodySchema: schema?.body,
|
|
@@ -1676,9 +1710,9 @@ export class Server {
|
|
|
1676
1710
|
authStages: [...compiled.authStages],
|
|
1677
1711
|
decorations: routeDecorations,
|
|
1678
1712
|
hasDecorations,
|
|
1679
|
-
beforeHandle: [...
|
|
1680
|
-
afterHandle: [...
|
|
1681
|
-
onError: [...
|
|
1713
|
+
beforeHandle: [...beforeHandle],
|
|
1714
|
+
afterHandle: [...afterHandle],
|
|
1715
|
+
onError: [...onError],
|
|
1682
1716
|
lifecycleHookLane: lanes.lifecycleHookLane,
|
|
1683
1717
|
around: [...this.aroundHooks],
|
|
1684
1718
|
execution,
|
|
@@ -2064,6 +2098,7 @@ export class Server {
|
|
|
2064
2098
|
// Cleanup the source registered (a pool, a queue consumer) belongs to the server that now runs
|
|
2065
2099
|
// its routes; dropping it would leak the resource past `stop()`.
|
|
2066
2100
|
this.stopHooks.push(...source.stopHooks);
|
|
2101
|
+
this.stoppingHooks.push(...source.stoppingHooks);
|
|
2067
2102
|
this.mcpResourceList.push(...source.mcpResourceList);
|
|
2068
2103
|
this.mcpPromptList.push(...source.mcpPromptList);
|
|
2069
2104
|
}
|
|
@@ -2153,7 +2188,7 @@ export class Server {
|
|
|
2153
2188
|
fetch(req, platform) {
|
|
2154
2189
|
// Deno's `Request.url` is the target as sent; Bun, workerd and every `new Request()` parse it.
|
|
2155
2190
|
// Route what the parsed URL says, so `/users/../admin` is `/admin` on every runtime.
|
|
2156
|
-
const target = requestTarget(req);
|
|
2191
|
+
const target = requestTarget(req, this.validateHost);
|
|
2157
2192
|
if (target === true)
|
|
2158
2193
|
return jsonError(400, "invalid_host");
|
|
2159
2194
|
if (target !== false) {
|
|
@@ -2562,7 +2597,7 @@ export class Server {
|
|
|
2562
2597
|
// The adapter answers a pass by handing this same object to `fetch`.
|
|
2563
2598
|
const received = req;
|
|
2564
2599
|
// The handshake routes the path `fetch` would: Host checked, dot segments resolved (see `fetch`).
|
|
2565
|
-
const target = requestTarget(req);
|
|
2600
|
+
const target = requestTarget(req, this.validateHost);
|
|
2566
2601
|
if (target === true) {
|
|
2567
2602
|
return { kind: "reject", response: this.wrapWebResponse(jsonError(400, "invalid_host")) };
|
|
2568
2603
|
}
|
|
@@ -2691,16 +2726,20 @@ export class Server {
|
|
|
2691
2726
|
return refuse(jsonError(500, "internal_error"));
|
|
2692
2727
|
}
|
|
2693
2728
|
};
|
|
2694
|
-
const child = mount.authStages.length === 0
|
|
2695
|
-
? upgrade()
|
|
2696
|
-
: thenMaybe(authenticateMount(mount.authStages, request, platform, this.logger), (refusal) => refusal === undefined ? upgrade() : refuse(refusal));
|
|
2697
2729
|
const settle = (outcome) => outcome.kind === "pass" ||
|
|
2698
2730
|
(outcome.kind === "reject" && outcome.response.status === 404 && selected.fallbackOn404)
|
|
2699
2731
|
? dispatch(selectedIndex + 1)
|
|
2700
2732
|
: outcome.kind === "reject"
|
|
2701
2733
|
? refuse(outcome.response)
|
|
2702
2734
|
: outcome;
|
|
2703
|
-
|
|
2735
|
+
const run = () => {
|
|
2736
|
+
const child = upgrade();
|
|
2737
|
+
return child instanceof Promise ? child.then(settle) : settle(child);
|
|
2738
|
+
};
|
|
2739
|
+
// Only a child's miss may fall through, never an authentication refusal.
|
|
2740
|
+
return mount.authStages.length === 0
|
|
2741
|
+
? run()
|
|
2742
|
+
: thenMaybe(authenticateMount(mount.authStages, request, platform, this.logger), (refusal) => (refusal === undefined ? run() : refuse(refusal)));
|
|
2704
2743
|
};
|
|
2705
2744
|
return dispatch(0);
|
|
2706
2745
|
};
|
|
@@ -3094,20 +3133,26 @@ export class Server {
|
|
|
3094
3133
|
return fallback ?? jsonError(404, "not_found");
|
|
3095
3134
|
const selected = mount;
|
|
3096
3135
|
const run = () => {
|
|
3136
|
+
const reused = selected.stripPrefix ? selected.wsPassed?.get(request) : undefined;
|
|
3097
3137
|
const childRequest = selected.stripPrefix
|
|
3098
|
-
? (
|
|
3138
|
+
? (reused ?? stripMountPrefix(request, selected.path))
|
|
3099
3139
|
: request;
|
|
3100
|
-
|
|
3140
|
+
// HTTP authentication may have marked the original since the WebSocket pass.
|
|
3141
|
+
if (reused !== undefined && isAuthenticatedRequest(request)) {
|
|
3142
|
+
markAuthenticatedRequest(reused);
|
|
3143
|
+
}
|
|
3144
|
+
const outcome = selected.handler(childRequest, platform);
|
|
3145
|
+
if (!replayable || !selected.fallbackOn404)
|
|
3146
|
+
return outcome;
|
|
3147
|
+
if (outcome instanceof Promise) {
|
|
3148
|
+
return outcome.then((response) => response.status === 404 ? dispatch(index + 1, response) : response);
|
|
3149
|
+
}
|
|
3150
|
+
return outcome.status === 404 ? dispatch(index + 1, outcome) : outcome;
|
|
3101
3151
|
};
|
|
3102
|
-
|
|
3152
|
+
// Keep gate refusals outside the child's 404 fallthrough path.
|
|
3153
|
+
return mount.authStages.length === 0
|
|
3103
3154
|
? run()
|
|
3104
3155
|
: thenMaybe(authenticateMount(mount.authStages, request, platform, this.logger), (refusal) => refusal ?? run());
|
|
3105
|
-
if (!replayable || !mount.fallbackOn404)
|
|
3106
|
-
return outcome;
|
|
3107
|
-
if (outcome instanceof Promise) {
|
|
3108
|
-
return outcome.then((response) => response.status === 404 ? dispatch(index + 1, response) : response);
|
|
3109
|
-
}
|
|
3110
|
-
return outcome.status === 404 ? dispatch(index + 1, outcome) : outcome;
|
|
3111
3156
|
};
|
|
3112
3157
|
return dispatch(first);
|
|
3113
3158
|
}
|
|
@@ -4690,8 +4735,12 @@ export class Server {
|
|
|
4690
4735
|
const paramNames = pattern.paramNames;
|
|
4691
4736
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined;
|
|
4692
4737
|
const handler = this.compileBunNativeHandler(entry, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback);
|
|
4693
|
-
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it.
|
|
4694
|
-
|
|
4738
|
+
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it. Left
|
|
4739
|
+
// off, `validateHost` costs nothing here: the table holds no wildcard, a `..` parameter falls
|
|
4740
|
+
// back, and reading the Host on every request measured about 6% of a bare route.
|
|
4741
|
+
tableAt(path, pattern.segments)[method] = this.validateHost
|
|
4742
|
+
? (request, server) => requestTarget(request, true) ? fallback(request, server) : handler(request, server)
|
|
4743
|
+
: handler;
|
|
4695
4744
|
count += 1;
|
|
4696
4745
|
if (paramNames.length > 0) {
|
|
4697
4746
|
below.add(pattern.segments.length + method);
|
|
@@ -4874,11 +4923,12 @@ export class Server {
|
|
|
4874
4923
|
* The Bun semantics: poll `pendingRequests` (awaiting
|
|
4875
4924
|
* `stop()`'s promise drops in-flight requests), and decide graceful-vs-forced in
|
|
4876
4925
|
* ONE call (Bun can't escalate an already-graceful `stop()` to a forced close).
|
|
4877
|
-
* New connections may be accepted during the drain window;
|
|
4878
|
-
*
|
|
4879
|
-
*
|
|
4926
|
+
* New connections may be accepted during the drain window; `delayMs` is the time a load balancer
|
|
4927
|
+
* gets to stop routing here first. The `onStopping` hooks run before that delay, the `onStop`
|
|
4928
|
+
* hooks after the drain. Open WebSockets get a 1001 close when the drain starts and the same
|
|
4929
|
+
* window to finish it; a socket opened after `stop()` began is closed at once.
|
|
4880
4930
|
*/
|
|
4881
|
-
async stop({ drainMs = DEFAULT_DRAIN_MS } = {}) {
|
|
4931
|
+
async stop({ drainMs = DEFAULT_DRAIN_MS, delayMs = 0 } = {}) {
|
|
4882
4932
|
const onSignal = this.signalHandler;
|
|
4883
4933
|
if (onSignal !== undefined) {
|
|
4884
4934
|
this.signalHandler = undefined;
|
|
@@ -4888,6 +4938,9 @@ export class Server {
|
|
|
4888
4938
|
if (server === undefined)
|
|
4889
4939
|
return;
|
|
4890
4940
|
this.bunServer = undefined;
|
|
4941
|
+
await this[SERVER_LIFECYCLE]("stopping");
|
|
4942
|
+
if (delayMs > 0)
|
|
4943
|
+
await Bun.sleep(delayMs);
|
|
4891
4944
|
const sockets = this.bunSockets;
|
|
4892
4945
|
this.bunSockets = undefined;
|
|
4893
4946
|
if (sockets !== undefined)
|
|
@@ -4899,14 +4952,25 @@ export class Server {
|
|
|
4899
4952
|
await Bun.sleep(DRAIN_POLL_MS);
|
|
4900
4953
|
}
|
|
4901
4954
|
server.stop(busy()); // force-close iff stragglers remain past the deadline
|
|
4902
|
-
|
|
4955
|
+
await this[SERVER_LIFECYCLE]("stopped");
|
|
4956
|
+
}
|
|
4957
|
+
/**
|
|
4958
|
+
* @internal Run the `onStopping` or the `onStop` hooks, each bounded. A runtime adapter that drains
|
|
4959
|
+
* its own listener calls it on either side of the drain, as `stop()` does here.
|
|
4960
|
+
*/
|
|
4961
|
+
async [SERVER_LIFECYCLE](phase) {
|
|
4962
|
+
const hooks = phase === "stopping" ? this.stoppingHooks : this.stopHooks;
|
|
4963
|
+
if (hooks.length === 0)
|
|
4903
4964
|
return;
|
|
4904
|
-
|
|
4905
|
-
const settled = Promise.allSettled(callbacks);
|
|
4965
|
+
let timer;
|
|
4906
4966
|
await Promise.race([
|
|
4907
|
-
|
|
4908
|
-
new Promise((resolve) =>
|
|
4967
|
+
Promise.allSettled(hooks.map((hook) => Promise.resolve().then(hook))),
|
|
4968
|
+
new Promise((resolve) => {
|
|
4969
|
+
timer = setTimeout(resolve, STOP_HOOK_TIMEOUT_MS);
|
|
4970
|
+
}),
|
|
4909
4971
|
]);
|
|
4972
|
+
// A pending bound would hold the event loop open after every hook settled.
|
|
4973
|
+
clearTimeout(timer);
|
|
4910
4974
|
}
|
|
4911
4975
|
installSignalHandlers(drain) {
|
|
4912
4976
|
// Drain, then let the process exit naturally - the stopped server no longer
|