@nifrajs/core 5.3.0 → 5.5.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 +65 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/route-lanes.d.ts.map +1 -1
- package/dist/internal/route-lanes.js +2 -1
- package/dist/internal/route-lanes.js.map +1 -1
- package/dist/internal/route-program.d.ts +5 -0
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +36 -15
- package/dist/internal/route-program.js.map +1 -1
- package/dist/range.d.ts.map +1 -1
- package/dist/range.js +62 -2
- package/dist/range.js.map +1 -1
- package/dist/router/router.d.ts +3 -2
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +22 -6
- package/dist/router/router.js.map +1 -1
- package/dist/security/baseline.d.ts.map +1 -1
- package/dist/security/baseline.js +4 -3
- package/dist/security/baseline.js.map +1 -1
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +4 -3
- package/dist/server/body-lane.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 +5 -0
- package/dist/server/body.js.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +9 -2
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/idempotency-lane.d.ts +11 -12
- package/dist/server/idempotency-lane.d.ts.map +1 -1
- package/dist/server/idempotency-lane.js +301 -151
- package/dist/server/idempotency-lane.js.map +1 -1
- package/dist/server/methods.d.ts +3 -2
- package/dist/server/methods.d.ts.map +1 -1
- package/dist/server/methods.js.map +1 -1
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/multipart.js +42 -13
- package/dist/server/multipart.js.map +1 -1
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +2 -2
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/query.d.ts +1 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +13 -5
- package/dist/server/query.js.map +1 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +3 -3
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +17 -0
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +81 -3
- package/dist/server/respond.js.map +1 -1
- package/dist/server/runtime-core.d.ts +15 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +41 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server.d.ts +14 -5
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +187 -62
- package/dist/server/server.js.map +1 -1
- package/dist/server/transport-lane.d.ts +2 -2
- package/dist/server/transport-lane.d.ts.map +1 -1
- package/dist/server/transport-lane.js +20 -20
- package/dist/server/transport-lane.js.map +1 -1
- package/dist/static-file.d.ts +127 -0
- package/dist/static-file.d.ts.map +1 -0
- package/dist/static-file.js +269 -0
- package/dist/static-file.js.map +1 -0
- package/package.json +6 -1
- package/src/index.ts +1 -1
- package/src/internal/route-lanes.ts +18 -16
- package/src/internal/route-program.ts +76 -12
- package/src/range.ts +61 -2
- package/src/router/router.ts +26 -5
- package/src/security/baseline.ts +4 -3
- package/src/server/body-lane.ts +4 -4
- package/src/server/body.ts +6 -0
- package/src/server/cookies.ts +7 -2
- package/src/server/core-copies.ts +1 -1
- package/src/server/idempotency-lane.ts +343 -209
- package/src/server/methods.ts +3 -2
- package/src/server/multipart.ts +62 -14
- package/src/server/node-outcome.ts +2 -1
- package/src/server/query.ts +12 -5
- package/src/server/request-context.ts +6 -3
- package/src/server/respond.ts +108 -6
- package/src/server/runtime-core.ts +54 -1
- package/src/server/server.ts +257 -102
- package/src/server/transport-lane.ts +28 -22
- package/src/static-file.ts +362 -0
package/dist/server/server.js
CHANGED
|
@@ -8,7 +8,7 @@ import { adoptRouteRuntimes, compileRouteOptions } from "../internal/route-compi
|
|
|
8
8
|
import { compileRouteExecutionPlan, } from "../internal/route-execution.js";
|
|
9
9
|
import { compileRouteProgram, executeRouteProgram } from "../internal/route-program.js";
|
|
10
10
|
import { isSameOriginRequest } from "../internal/same-origin.js";
|
|
11
|
-
import { decodeRouteParams, expandOptionalParams } from "../router/pattern.js";
|
|
11
|
+
import { decodeRouteParams, expandOptionalParams, } from "../router/pattern.js";
|
|
12
12
|
import { EMPTY_PARAMS, METHODS, Router } from "../router/router.js";
|
|
13
13
|
import { emitRequestErrorLog, renderBareError } from "./bare-error-lane.js";
|
|
14
14
|
import { assertByteLimit, markTransportCap, markTrustedBodyFraming, UNLIMITED_BODY_BYTES, } from "./body.js";
|
|
@@ -22,7 +22,7 @@ import { queryObjectOf, searchOf } from "./query.js";
|
|
|
22
22
|
import { PLATFORM_PEER, RequestContext, readBodyFramed, } from "./request-context.js";
|
|
23
23
|
import { recordRequestReplacement } from "./request-lineage.js";
|
|
24
24
|
import { needsResolving, resolveDotSegments } from "./request-target.js";
|
|
25
|
-
import { applyStaticResponseHeaders, buildStaticResponseHeaders, fusedRespond, fusedRespondNoSet, toResponse, writableResponse, } from "./respond.js";
|
|
25
|
+
import { applyStaticResponseHeaders, buildStaticResponseHeaders, claimHandedOver, fusedRespond, fusedRespondNoSet, handedBackResponse, recordResidentTexts, toResponse, writableResponse, } from "./respond.js";
|
|
26
26
|
import { CONTEXT_REQUEST, CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, } from "./runtime-core.js";
|
|
27
27
|
import { normalizeStaticResponseHeaders } from "./static-headers.js";
|
|
28
28
|
import { plainValidationError } from "./validation.js";
|
|
@@ -54,6 +54,9 @@ function bunPeerPlatform(server, req) {
|
|
|
54
54
|
},
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
// Bun's route table has a slot for each of these; a QUERY entry is ignored and the request falls
|
|
58
|
+
// through to the table's fetch, so QUERY routes stay on the portable dispatcher.
|
|
59
|
+
const BUN_TABLE_METHODS = METHODS.filter((method) => method !== "QUERY");
|
|
57
60
|
/**
|
|
58
61
|
* Whether the portable router gives `other` a request that `route` also matches: the two patterns can
|
|
59
62
|
* match one path, and `other` is the more specific at the first segment where they differ. `route`
|
|
@@ -80,6 +83,23 @@ function outranks(other, route) {
|
|
|
80
83
|
}
|
|
81
84
|
return ahead && other.length === route.length;
|
|
82
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The key Bun's route table holds `path` under. Bun refuses one with a character outside printable
|
|
88
|
+
* ASCII, so such a path is spelled as the portable router matches it: percent-encoded static
|
|
89
|
+
* segments and `:name` parameters. `segments` holds static and `:param` segments only.
|
|
90
|
+
*/
|
|
91
|
+
function bunTablePath(path, segments) {
|
|
92
|
+
if (!/[^ -~]/.test(path))
|
|
93
|
+
return path;
|
|
94
|
+
let key = "";
|
|
95
|
+
for (const segment of segments) {
|
|
96
|
+
if (segment.kind === "static")
|
|
97
|
+
key += `/${segment.value}`;
|
|
98
|
+
else if (segment.kind === "param")
|
|
99
|
+
key += `/:${segment.name}`;
|
|
100
|
+
}
|
|
101
|
+
return key;
|
|
102
|
+
}
|
|
83
103
|
const WS_PASS = { kind: "pass" };
|
|
84
104
|
const DEFAULT_WS_UPGRADE_TIMEOUT_MS = 10_000;
|
|
85
105
|
/** Counts `ws()` routes and `mount()`s across every app in this copy of core: the only
|
|
@@ -157,8 +177,9 @@ const withResolvedTarget = (req) => new Request(resolveDotSegments(req.url), req
|
|
|
157
177
|
// `Request.url` by appending the target to the Host as received, so `Host: x/admin#` turned a
|
|
158
178
|
// request for `/public` into one for `/admin`. Node refuses such a Host before routing; so does this.
|
|
159
179
|
const HOST_AUTHORITY = /^(?:\[[0-9a-f:.%]+\]|[\w.-]+)(?::\d{1,5})?$/i;
|
|
180
|
+
// `headerOf`, not `req.headers`: on Deno that reads the Host alone instead of building every header.
|
|
160
181
|
const invalidHost = (req) => {
|
|
161
|
-
const host = req
|
|
182
|
+
const host = headerOf(req, "host");
|
|
162
183
|
return host !== null && !HOST_AUTHORITY.test(host);
|
|
163
184
|
};
|
|
164
185
|
function normalizeMountPrefix(path) {
|
|
@@ -287,6 +308,12 @@ const SERVER_RUNTIMES = [
|
|
|
287
308
|
// Stable module-level finalizers so `fetch`/`resolveNode` allocate no per-request closures.
|
|
288
309
|
const IDENTITY_RESPONSE = (response) => webResponseOf(response);
|
|
289
310
|
const RESPONSE_TIMEOUT = () => jsonError(503, "request_timeout");
|
|
311
|
+
/** An `onRequest` hook's answer, held to the single-use check a handler's result gets. */
|
|
312
|
+
function earlyAnswer(answer, wrapResponse) {
|
|
313
|
+
return answer instanceof Response && !claimHandedOver(answer)
|
|
314
|
+
? wrapResponse(plainError(500, "internal_error"))
|
|
315
|
+
: wrapResponse(answer);
|
|
316
|
+
}
|
|
290
317
|
// The unused-order-scoped-hook audit (recorder, seal check, stack capture, message) is a dev-time
|
|
291
318
|
// diagnostic. Every guard below keeps the INLINE literal `process.env.NODE_ENV !== "production"` at the
|
|
292
319
|
// branch - never a shared const holding the folded boolean - because a bundler's `process.env.NODE_ENV`
|
|
@@ -327,6 +354,9 @@ const captureCallerSite = () => {
|
|
|
327
354
|
return undefined;
|
|
328
355
|
};
|
|
329
356
|
const hookAudit = /* @__PURE__ */ new WeakMap();
|
|
357
|
+
// Audits begun and not yet sealed. Zero outside development, so a request tests it before the
|
|
358
|
+
// NODE_ENV literal and never reads `process.env` (a native call of ~130 ns on Node).
|
|
359
|
+
let unsealedHookAudits = 0;
|
|
330
360
|
/** Whether `from` reaches `target` through mounts that keep the path and take a request under `path`.
|
|
331
361
|
* Module-level, not a `Server` method, so a production bundle drops it with its dev-only caller. */
|
|
332
362
|
function mountLeadsBack(from, target, path, mountsOf, seen) {
|
|
@@ -344,6 +374,7 @@ function mountLeadsBack(from, target, path, mountsOf, seen) {
|
|
|
344
374
|
const PAGES_DECLARED = /* @__PURE__ */ Symbol.for("nifra.web.pagesDeclared");
|
|
345
375
|
/** Start tracking one server's order-scoped hook pushes (once, from the constructor). */
|
|
346
376
|
const beginHookAudit = (key, policy, hasCustomLogger) => {
|
|
377
|
+
unsealedHookAudits++;
|
|
347
378
|
hookAudit.set(key, {
|
|
348
379
|
policy,
|
|
349
380
|
hasCustomLogger,
|
|
@@ -434,6 +465,7 @@ const sealHookAudit = (key, total, sockets, logger) => {
|
|
|
434
465
|
if (state === undefined || state.checked)
|
|
435
466
|
return;
|
|
436
467
|
state.checked = true;
|
|
468
|
+
unsealedHookAudits--;
|
|
437
469
|
if (state.policy === "off" || total + sockets === 0)
|
|
438
470
|
return;
|
|
439
471
|
const own = total - state.merged;
|
|
@@ -554,6 +586,9 @@ export class Server {
|
|
|
554
586
|
/** App-wide validation-error fallback; a route's own `schema.onValidationError` takes precedence. */
|
|
555
587
|
defaultOnValidationError;
|
|
556
588
|
bunServer;
|
|
589
|
+
// Bun's `pendingRequests` leaves out upgraded sockets, so `stop()` closes these itself.
|
|
590
|
+
bunSockets;
|
|
591
|
+
signalHandler;
|
|
557
592
|
sealed;
|
|
558
593
|
derives;
|
|
559
594
|
/** Dedicated authentication stages captured by subsequent routes. */
|
|
@@ -895,7 +930,8 @@ export class Server {
|
|
|
895
930
|
/**
|
|
896
931
|
* Run before routing on the raw request. Return a `Response` or a `status()` result to
|
|
897
932
|
* short-circuit, or a replacement `Request` to continue routing with a rewritten method/URL/headers.
|
|
898
|
-
* Global.
|
|
933
|
+
* Global. A throw or rejection goes to every registered {@link onError} (route-less context), then
|
|
934
|
+
* the logged default 500.
|
|
899
935
|
*/
|
|
900
936
|
onRequest(fn) {
|
|
901
937
|
this.assertConfigurable("onRequest()");
|
|
@@ -981,8 +1017,8 @@ export class Server {
|
|
|
981
1017
|
}
|
|
982
1018
|
/** Handle a thrown error; a non-`undefined` return becomes the response (else the default 500, or 503
|
|
983
1019
|
* `auth_unavailable` for a failed {@link authenticate} stage). Order-scoped: captured by routes
|
|
984
|
-
* declared *after* this call, none before; one added after the last route reaches
|
|
985
|
-
* {@link ServerOptions.unusedScopedHooks}). */
|
|
1020
|
+
* declared *after* this call, none before; one added after the last route reaches no route (see
|
|
1021
|
+
* {@link ServerOptions.unusedScopedHooks}). Every one answers an {@link onRequest} failure. */
|
|
986
1022
|
onError(fn) {
|
|
987
1023
|
this.assertConfigurable("onError()");
|
|
988
1024
|
this.onErrorHooks.push(fn);
|
|
@@ -1075,6 +1111,7 @@ export class Server {
|
|
|
1075
1111
|
if (this.responseBodyTag === undefined) {
|
|
1076
1112
|
this.responseBodyTag = Object.freeze({});
|
|
1077
1113
|
this.responseBodyOwners.add(this.responseBodyTag);
|
|
1114
|
+
recordResidentTexts();
|
|
1078
1115
|
}
|
|
1079
1116
|
return this.responseBodyTag;
|
|
1080
1117
|
}
|
|
@@ -1572,6 +1609,7 @@ export class Server {
|
|
|
1572
1609
|
if (this.responseBodyTag === undefined) {
|
|
1573
1610
|
this.responseBodyTag = Object.freeze({});
|
|
1574
1611
|
this.responseBodyOwners.add(this.responseBodyTag);
|
|
1612
|
+
recordResidentTexts();
|
|
1575
1613
|
}
|
|
1576
1614
|
return this.responseBodyTag;
|
|
1577
1615
|
},
|
|
@@ -2007,8 +2045,8 @@ export class Server {
|
|
|
2007
2045
|
}
|
|
2008
2046
|
fetchSource(source, platform) {
|
|
2009
2047
|
// Seal once on the first request so test servers (no `listen()`) still get the dead-hook check.
|
|
2010
|
-
// After that it is
|
|
2011
|
-
if (
|
|
2048
|
+
// After that it is one integer compare; the literal stays so a production define drops the branch.
|
|
2049
|
+
if (unsealedHookAudits !== 0 && process.env.NODE_ENV !== "production")
|
|
2012
2050
|
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger);
|
|
2013
2051
|
// Off path (default): straight through - one property check, no closure, no promise.
|
|
2014
2052
|
if (this.capacityGate === undefined)
|
|
@@ -2093,6 +2131,9 @@ export class Server {
|
|
|
2093
2131
|
* a path whose registered route rejects this method.
|
|
2094
2132
|
*/
|
|
2095
2133
|
[RESOLVE_NODE_MOUNT](source) {
|
|
2134
|
+
// Node asks before every dispatch; an app with no mounts cannot select a native handler.
|
|
2135
|
+
if (this.fetchMounts.length === 0)
|
|
2136
|
+
return undefined;
|
|
2096
2137
|
if (this.onRequestHooks.length > 0 ||
|
|
2097
2138
|
this.onResponseHooks.length > 0 ||
|
|
2098
2139
|
this.onResponseFinalizedHooks.length > 0 ||
|
|
@@ -2141,7 +2182,7 @@ export class Server {
|
|
|
2141
2182
|
}
|
|
2142
2183
|
resolveNodeSource(source, platform, suppliedRuntime) {
|
|
2143
2184
|
// Seal once on the Node-direct lane too - it can bypass `fetchSource` entirely (hookless outcome).
|
|
2144
|
-
if (
|
|
2185
|
+
if (unsealedHookAudits !== 0 && process.env.NODE_ENV !== "production")
|
|
2145
2186
|
sealHookAudit(this, this.catalog.size, this.wsRoutes.length, this.logger);
|
|
2146
2187
|
// A paired header-only native hook can preserve the Node-direct JSON/body outcome for successful
|
|
2147
2188
|
// responses. Arbitrary onResponse transforms need a real Web Response, but a buffered outcome can
|
|
@@ -2381,11 +2422,11 @@ export class Server {
|
|
|
2381
2422
|
}
|
|
2382
2423
|
}
|
|
2383
2424
|
resolveWebSocketUpgrade(req, platform) {
|
|
2384
|
-
// Before any header is read
|
|
2385
|
-
// costs
|
|
2425
|
+
// Before any header is read, and then through `headerOf`: Deno builds a request's whole header
|
|
2426
|
+
// list when `headers` is read, which costs every plain request of an app with sockets measurably.
|
|
2386
2427
|
if (this.wsRoutes.length === 0 && !this.mountsTakeUpgrades())
|
|
2387
2428
|
return WS_PASS;
|
|
2388
|
-
if (req
|
|
2429
|
+
if (headerOf(req, "upgrade")?.toLowerCase() !== "websocket")
|
|
2389
2430
|
return WS_PASS;
|
|
2390
2431
|
// A WebSocket handshake is a GET (RFC 6455 section 4.1). Any other method with an Upgrade header
|
|
2391
2432
|
// is an ordinary request: its HTTP route answers it, and normal routing refuses an unknown token.
|
|
@@ -2547,17 +2588,24 @@ export class Server {
|
|
|
2547
2588
|
this.wsHookedPasses.set(received, request);
|
|
2548
2589
|
return resolveWithDeadline(request);
|
|
2549
2590
|
};
|
|
2591
|
+
// The same boundary as an HTTP request's hooks; the answer is already wrapped, so it is not refused.
|
|
2592
|
+
const failed = (err) => {
|
|
2593
|
+
const answer = this.onRequestFailure(err, req, platform, this.finalizeResponse, this.wrapWebResponse);
|
|
2594
|
+
return answer instanceof Promise
|
|
2595
|
+
? answer.then((response) => ({ kind: "reject", response }))
|
|
2596
|
+
: { kind: "reject", response: answer };
|
|
2597
|
+
};
|
|
2550
2598
|
let hooked;
|
|
2551
2599
|
try {
|
|
2552
2600
|
hooked = this.runWebSocketRequestHooks(req, platform);
|
|
2553
2601
|
}
|
|
2554
|
-
catch {
|
|
2555
|
-
return
|
|
2602
|
+
catch (err) {
|
|
2603
|
+
return failed(err);
|
|
2556
2604
|
}
|
|
2557
2605
|
if (!(hooked instanceof Promise)) {
|
|
2558
2606
|
return hooked instanceof Response ? refuse(hooked) : resolveHooked(hooked);
|
|
2559
2607
|
}
|
|
2560
|
-
const pending = hooked.then((result) => (result instanceof Response ? refuse(result) : resolveHooked(result)),
|
|
2608
|
+
const pending = hooked.then((result) => (result instanceof Response ? refuse(result) : resolveHooked(result)), failed);
|
|
2561
2609
|
return bounded(Promise.resolve(pending).then((result) => result), onTimeout);
|
|
2562
2610
|
}
|
|
2563
2611
|
/** Bun `fetch` when WS routes exist: try a WS upgrade first, else run the normal HTTP lifecycle.
|
|
@@ -2632,24 +2680,36 @@ export class Server {
|
|
|
2632
2680
|
const request = this.nodeRequestContextOf(source);
|
|
2633
2681
|
for (let i = 0; i < hooks.length; i++) {
|
|
2634
2682
|
const hook = hooks[i];
|
|
2635
|
-
|
|
2683
|
+
let outcome;
|
|
2684
|
+
try {
|
|
2685
|
+
outcome = hook(request, platform);
|
|
2686
|
+
}
|
|
2687
|
+
catch (err) {
|
|
2688
|
+
return this.onRequestFailure(err, source, platform, finalize, wrapResponse);
|
|
2689
|
+
}
|
|
2636
2690
|
if (outcome instanceof Promise) {
|
|
2637
|
-
return outcome.then((early) => this.continueNodeRequest(early, i + 1, request, source, platform, finalize, wrapResponse, onTimeout));
|
|
2691
|
+
return outcome.then((early) => this.continueNodeRequest(early, i + 1, request, source, platform, finalize, wrapResponse, onTimeout), (err) => this.onRequestFailure(err, source, platform, finalize, wrapResponse));
|
|
2638
2692
|
}
|
|
2639
2693
|
if (outcome !== undefined)
|
|
2640
|
-
return
|
|
2694
|
+
return earlyAnswer(outcome, wrapResponse);
|
|
2641
2695
|
}
|
|
2642
2696
|
return this.routeAndRun(source, platform, finalize, wrapResponse, onTimeout, false);
|
|
2643
2697
|
}
|
|
2644
2698
|
async continueNodeRequest(first, nextIndex, request, source, platform, finalize, wrapResponse, onTimeout) {
|
|
2645
2699
|
if (first !== undefined)
|
|
2646
|
-
return
|
|
2700
|
+
return earlyAnswer(first, wrapResponse);
|
|
2647
2701
|
for (let i = nextIndex; i < this.onNodeRequestHooks.length; i++) {
|
|
2648
2702
|
const hook = this.onNodeRequestHooks[i];
|
|
2649
|
-
|
|
2650
|
-
|
|
2703
|
+
let early;
|
|
2704
|
+
try {
|
|
2705
|
+
const outcome = hook(request, platform);
|
|
2706
|
+
early = outcome instanceof Promise ? await outcome : outcome;
|
|
2707
|
+
}
|
|
2708
|
+
catch (err) {
|
|
2709
|
+
return this.onRequestFailure(err, source, platform, finalize, wrapResponse);
|
|
2710
|
+
}
|
|
2651
2711
|
if (early !== undefined)
|
|
2652
|
-
return
|
|
2712
|
+
return earlyAnswer(early, wrapResponse);
|
|
2653
2713
|
}
|
|
2654
2714
|
return this.routeAndRun(source, platform, finalize, wrapResponse, onTimeout, false);
|
|
2655
2715
|
}
|
|
@@ -2674,9 +2734,15 @@ export class Server {
|
|
|
2674
2734
|
let current = source;
|
|
2675
2735
|
for (let i = 0; i < hooks.length; i++) {
|
|
2676
2736
|
const seen = requestOf(current);
|
|
2677
|
-
|
|
2737
|
+
let outcome;
|
|
2738
|
+
try {
|
|
2739
|
+
outcome = hooks[i](seen, platform);
|
|
2740
|
+
}
|
|
2741
|
+
catch (err) {
|
|
2742
|
+
return this.onRequestFailure(err, current, platform, finalize, wrapResponse);
|
|
2743
|
+
}
|
|
2678
2744
|
if (outcome instanceof Promise) {
|
|
2679
|
-
return outcome.then((early) => this.continueOnRequest(early, i + 1, originalRequest, current, platform, finalize, wrapResponse, onTimeout, webFast));
|
|
2745
|
+
return outcome.then((early) => this.continueOnRequest(early, i + 1, originalRequest, current, platform, finalize, wrapResponse, onTimeout, webFast), (err) => this.onRequestFailure(err, current, platform, finalize, wrapResponse));
|
|
2680
2746
|
}
|
|
2681
2747
|
if (outcome instanceof Request) {
|
|
2682
2748
|
if (outcome !== seen)
|
|
@@ -2687,7 +2753,7 @@ export class Server {
|
|
|
2687
2753
|
continue;
|
|
2688
2754
|
}
|
|
2689
2755
|
if (outcome !== undefined)
|
|
2690
|
-
return
|
|
2756
|
+
return earlyAnswer(outcome, wrapResponse);
|
|
2691
2757
|
}
|
|
2692
2758
|
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast);
|
|
2693
2759
|
}
|
|
@@ -2707,13 +2773,18 @@ export class Server {
|
|
|
2707
2773
|
this.responseRequests.set(originalRequest, early);
|
|
2708
2774
|
}
|
|
2709
2775
|
else if (early !== undefined) {
|
|
2710
|
-
return
|
|
2776
|
+
return earlyAnswer(early, wrapResponse);
|
|
2711
2777
|
}
|
|
2712
2778
|
if (index >= this.onRequestHooks.length)
|
|
2713
2779
|
break;
|
|
2714
2780
|
seen = requestOf(current);
|
|
2715
|
-
|
|
2716
|
-
|
|
2781
|
+
try {
|
|
2782
|
+
const outcome = this.onRequestHooks[index](seen, platform);
|
|
2783
|
+
early = outcome instanceof Promise ? await outcome : outcome;
|
|
2784
|
+
}
|
|
2785
|
+
catch (err) {
|
|
2786
|
+
return this.onRequestFailure(err, current, platform, finalize, wrapResponse);
|
|
2787
|
+
}
|
|
2717
2788
|
index++;
|
|
2718
2789
|
}
|
|
2719
2790
|
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast);
|
|
@@ -2928,14 +2999,14 @@ export class Server {
|
|
|
2928
2999
|
if (method !== "GET" && method !== "HEAD")
|
|
2929
3000
|
markTransportCap(source, entry.bodyLimit);
|
|
2930
3001
|
}
|
|
2931
|
-
// An idempotency route
|
|
2932
|
-
//
|
|
3002
|
+
// An idempotency route meets its dedupe gate inside the program, so the gate runs after
|
|
3003
|
+
// authentication and under the request deadline. Its lanes render a `Response`, which the gate
|
|
3004
|
+
// stores. Non-idempotent routes skip straight to the lanes - no added cost.
|
|
2933
3005
|
// The runtime is always present when a route resolved idempotency (enforced at registration).
|
|
2934
3006
|
if (entry.idempotent !== undefined && this.idempotencyRuntime !== undefined) {
|
|
2935
|
-
return this.idempotencyRuntime
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
});
|
|
3007
|
+
return this.idempotencyRuntime
|
|
3008
|
+
.run(source, platform, (prepared) => this.runMatchedLanes(prepared, platform, entry, params, search, this.finalizeResponse, this.wrapWebResponse, this.webResponseTimeout, false))
|
|
3009
|
+
.then(wrapResponse);
|
|
2939
3010
|
}
|
|
2940
3011
|
return this.runMatchedLanes(source, platform, entry, params, search, finalize, wrapResponse, onTimeout, webFast);
|
|
2941
3012
|
}
|
|
@@ -4026,7 +4097,7 @@ export class Server {
|
|
|
4026
4097
|
const handed = current;
|
|
4027
4098
|
return next.then((settled) => this.continueOnResponse(settled, i + 1, req, handed));
|
|
4028
4099
|
}
|
|
4029
|
-
current = next === current ? next :
|
|
4100
|
+
current = next === current ? next : handedBackResponse(next, req);
|
|
4030
4101
|
}
|
|
4031
4102
|
return current;
|
|
4032
4103
|
}
|
|
@@ -4086,11 +4157,11 @@ export class Server {
|
|
|
4086
4157
|
}
|
|
4087
4158
|
/** Async tail of {@link applyOnResponse}: runs the remaining hooks once one has gone async. */
|
|
4088
4159
|
async continueOnResponse(response, nextIndex, req, handed) {
|
|
4089
|
-
let current = response === handed ? response :
|
|
4160
|
+
let current = response === handed ? response : handedBackResponse(response, req);
|
|
4090
4161
|
for (let i = nextIndex; i < this.onResponseHooks.length; i++) {
|
|
4091
4162
|
const next = this.onResponseHooks[i](current, req);
|
|
4092
4163
|
const settled = next instanceof Promise ? await next : next;
|
|
4093
|
-
current = settled === current ? settled :
|
|
4164
|
+
current = settled === current ? settled : handedBackResponse(settled, req);
|
|
4094
4165
|
}
|
|
4095
4166
|
return current;
|
|
4096
4167
|
}
|
|
@@ -4172,28 +4243,57 @@ export class Server {
|
|
|
4172
4243
|
this.logRequestError(err, ctx);
|
|
4173
4244
|
return fallback === undefined ? wrapResponse(plainError(500, "internal_error")) : fallback();
|
|
4174
4245
|
}
|
|
4175
|
-
return this.runErrorHooks(entry, err, ctx, finalize, wrapResponse, fallback);
|
|
4246
|
+
return this.runErrorHooks(entry.onError, entry.responseContract, err, ctx, finalize, wrapResponse, fallback);
|
|
4247
|
+
}
|
|
4248
|
+
/** An `onRequest` hook threw or rejected. No route has matched, so every `onError` the app holds
|
|
4249
|
+
* answers it, with a route-less context; otherwise the logged default 500, as for a handler. */
|
|
4250
|
+
onRequestFailure(err, source, platform, finalize, wrapResponse) {
|
|
4251
|
+
if (err instanceof Response || isResponseResult(err))
|
|
4252
|
+
return wrapResponse(err);
|
|
4253
|
+
const ctx = new RequestContext(source, EMPTY_PARAMS, undefined, getNeverAbortSignal(), getUnboundedRequestBudget(), platform, this.maxBodyBytes, this.protoPoisoning);
|
|
4254
|
+
if (this.onErrorHooks.length === 0) {
|
|
4255
|
+
this.logRequestError(err, ctx);
|
|
4256
|
+
return this.internalErrorResponse(wrapResponse);
|
|
4257
|
+
}
|
|
4258
|
+
Object.assign(ctx, this.decorations);
|
|
4259
|
+
return this.runErrorHooks(this.onErrorHooks, undefined, err, ctx, finalize, wrapResponse);
|
|
4176
4260
|
}
|
|
4177
4261
|
/** The `onError` chain, split off {@link handleLifecycleError} so its `await` costs only the routes
|
|
4178
|
-
* that registered a hook. A hook may return a custom response; otherwise the default 500 stands.
|
|
4179
|
-
|
|
4262
|
+
* that registered a hook. A hook may return a custom response; otherwise the default 500 stands.
|
|
4263
|
+
* Never rejects: nothing outside it re-enters `onError`, so a hook that throws, or an answer that
|
|
4264
|
+
* cannot be rendered, is logged and answered here. */
|
|
4265
|
+
async runErrorHooks(hooks, contract, err, ctx, finalize, wrapResponse, fallback) {
|
|
4180
4266
|
// Indexed for the same reason as the `beforeHandle` chain above: the iterator would outlive
|
|
4181
4267
|
// the `await` and stay heap-allocated on both engines.
|
|
4182
|
-
for (let i = 0; i <
|
|
4183
|
-
|
|
4184
|
-
|
|
4268
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
4269
|
+
let handled;
|
|
4270
|
+
try {
|
|
4271
|
+
const outcome = hooks[i](err, ctx);
|
|
4272
|
+
handled = outcome instanceof Promise ? await outcome : outcome;
|
|
4273
|
+
}
|
|
4274
|
+
catch (hookError) {
|
|
4275
|
+
// A thrown Response or status() is the hook's answer, as from beforeHandle.
|
|
4276
|
+
if (!(hookError instanceof Response) && !isResponseResult(hookError)) {
|
|
4277
|
+
this.logRequestError(err, ctx);
|
|
4278
|
+
this.logRequestError(hookError, ctx);
|
|
4279
|
+
return fallback === undefined
|
|
4280
|
+
? wrapResponse(plainError(500, "internal_error"))
|
|
4281
|
+
: fallback();
|
|
4282
|
+
}
|
|
4283
|
+
handled = hookError;
|
|
4284
|
+
}
|
|
4185
4285
|
if (handled === undefined)
|
|
4186
4286
|
continue;
|
|
4187
|
-
|
|
4188
|
-
if (contract === undefined || (handled !== ctx.jsonReply && !isResponseResult(handled)))
|
|
4189
|
-
return finalize(handled, responseSet(ctx), ctx);
|
|
4190
|
-
// Checked here, not through finishLifecycleContract: a throwing schema must not re-enter onError.
|
|
4287
|
+
// Awaited so an asynchronous finalize's rejection lands in the catch too.
|
|
4191
4288
|
try {
|
|
4289
|
+
if (contract === undefined || (handled !== ctx.jsonReply && !isResponseResult(handled)))
|
|
4290
|
+
return await finalize(handled, responseSet(ctx), ctx);
|
|
4291
|
+
// Checked here, not through finishLifecycleContract: a throwing schema must not re-enter onError.
|
|
4192
4292
|
const checked = await contract.runtime.check(contract.definition, handled, ctx);
|
|
4193
|
-
return this.finishContractOutcome(ctx, finalize, wrapResponse, checked);
|
|
4293
|
+
return await this.finishContractOutcome(ctx, finalize, wrapResponse, checked);
|
|
4194
4294
|
}
|
|
4195
|
-
catch (
|
|
4196
|
-
this.logRequestError(
|
|
4295
|
+
catch (finishError) {
|
|
4296
|
+
this.logRequestError(finishError, ctx);
|
|
4197
4297
|
return wrapResponse(plainError(500, "internal_error"));
|
|
4198
4298
|
}
|
|
4199
4299
|
}
|
|
@@ -4408,9 +4508,10 @@ export class Server {
|
|
|
4408
4508
|
// Where Bun could pass a request down: the methods served natively on a path with a parameter,
|
|
4409
4509
|
// as segment count + method. Two table paths match one request only at the same segment count.
|
|
4410
4510
|
const below = new Set();
|
|
4411
|
-
const tableAt = (path) => {
|
|
4412
|
-
|
|
4413
|
-
|
|
4511
|
+
const tableAt = (path, segments) => {
|
|
4512
|
+
const key = bunTablePath(path, segments);
|
|
4513
|
+
routes[key] ??= Object.create(null);
|
|
4514
|
+
return routes[key];
|
|
4414
4515
|
};
|
|
4415
4516
|
for (let i = 0; i < all.length; i++) {
|
|
4416
4517
|
const { method, path, pattern, entry } = all[i];
|
|
@@ -4422,14 +4523,14 @@ export class Server {
|
|
|
4422
4523
|
// the platform the table shares could not answer `c.clientIp` there.
|
|
4423
4524
|
if (run === undefined ||
|
|
4424
4525
|
entry.idempotent !== undefined ||
|
|
4425
|
-
!
|
|
4526
|
+
!BUN_TABLE_METHODS.includes(method) ||
|
|
4426
4527
|
(preflightOnFallback && method === "OPTIONS") ||
|
|
4427
4528
|
under.get(run)?.some((other) => outranks(other.pattern.segments, pattern.segments))) {
|
|
4428
4529
|
continue;
|
|
4429
4530
|
}
|
|
4430
4531
|
const paramNames = pattern.paramNames;
|
|
4431
4532
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined;
|
|
4432
|
-
tableAt(path)[method] = this.compileBunNativeHandler(entry, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback);
|
|
4533
|
+
tableAt(path, pattern.segments)[method] = this.compileBunNativeHandler(entry, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback);
|
|
4433
4534
|
count += 1;
|
|
4434
4535
|
if (paramNames.length > 0) {
|
|
4435
4536
|
below.add(pattern.segments.length + method);
|
|
@@ -4456,9 +4557,9 @@ export class Server {
|
|
|
4456
4557
|
if (lead[i] === undefined)
|
|
4457
4558
|
continue;
|
|
4458
4559
|
const { path, pattern } = all[i];
|
|
4459
|
-
for (const method of
|
|
4560
|
+
for (const method of BUN_TABLE_METHODS) {
|
|
4460
4561
|
if (below.has(pattern.segments.length + method))
|
|
4461
|
-
tableAt(path)[method] ??= fallback;
|
|
4562
|
+
tableAt(path, pattern.segments)[method] ??= fallback;
|
|
4462
4563
|
}
|
|
4463
4564
|
}
|
|
4464
4565
|
return routes;
|
|
@@ -4555,6 +4656,8 @@ export class Server {
|
|
|
4555
4656
|
[PLATFORM_PEER]: (request) => running.requestIP(request)?.address,
|
|
4556
4657
|
};
|
|
4557
4658
|
const nativeRoutes = wsHandlers === undefined ? this.buildBunNativeRoutes(fallback, peer) : undefined;
|
|
4659
|
+
const sockets = new Set();
|
|
4660
|
+
this.bunSockets = sockets;
|
|
4558
4661
|
const running = (wsHandlers === undefined
|
|
4559
4662
|
? Bun.serve({
|
|
4560
4663
|
...listening,
|
|
@@ -4572,9 +4675,18 @@ export class Server {
|
|
|
4572
4675
|
// closes an over-cap connection before the handler runs. Default 1 MB (maxBodyBytes). One
|
|
4573
4676
|
// limit per listener: the largest cap among this app and the apps mounted on it.
|
|
4574
4677
|
maxPayloadLength: wsHandlers.maxPayloadLength,
|
|
4575
|
-
open: (ws) =>
|
|
4678
|
+
open: (ws) => {
|
|
4679
|
+
sockets.add(ws);
|
|
4680
|
+
wsHandlers.open(ws);
|
|
4681
|
+
// An upgrade accepted while `stop()` drains gets the close the earlier sockets got.
|
|
4682
|
+
if (this.bunServer !== running)
|
|
4683
|
+
ws.close(1001);
|
|
4684
|
+
},
|
|
4576
4685
|
message: (ws, message) => wsHandlers.message(ws, message),
|
|
4577
|
-
close: (ws, code, reason) =>
|
|
4686
|
+
close: (ws, code, reason) => {
|
|
4687
|
+
sockets.delete(ws);
|
|
4688
|
+
wsHandlers.close(ws, code, reason);
|
|
4689
|
+
},
|
|
4578
4690
|
},
|
|
4579
4691
|
}));
|
|
4580
4692
|
this.bunServer = running;
|
|
@@ -4602,18 +4714,30 @@ export class Server {
|
|
|
4602
4714
|
* `stop()`'s promise drops in-flight requests), and decide graceful-vs-forced in
|
|
4603
4715
|
* ONE call (Bun can't escalate an already-graceful `stop()` to a forced close).
|
|
4604
4716
|
* New connections may be accepted during the drain window; in a real deploy the
|
|
4605
|
-
* load balancer has already stopped routing here, and `drainMs` bounds it.
|
|
4717
|
+
* load balancer has already stopped routing here, and `drainMs` bounds it. Open WebSockets
|
|
4718
|
+
* get a 1001 close at once and the same window to finish it, before any `onStop` hook runs.
|
|
4606
4719
|
*/
|
|
4607
4720
|
async stop({ drainMs = DEFAULT_DRAIN_MS } = {}) {
|
|
4721
|
+
const onSignal = this.signalHandler;
|
|
4722
|
+
if (onSignal !== undefined) {
|
|
4723
|
+
this.signalHandler = undefined;
|
|
4724
|
+
process.off("SIGTERM", onSignal).off("SIGINT", onSignal);
|
|
4725
|
+
}
|
|
4608
4726
|
const server = this.bunServer;
|
|
4609
4727
|
if (server === undefined)
|
|
4610
4728
|
return;
|
|
4611
4729
|
this.bunServer = undefined;
|
|
4730
|
+
const sockets = this.bunSockets;
|
|
4731
|
+
this.bunSockets = undefined;
|
|
4732
|
+
if (sockets !== undefined)
|
|
4733
|
+
for (const ws of sockets)
|
|
4734
|
+
ws.close(1001);
|
|
4735
|
+
const busy = () => server.pendingRequests + (sockets?.size ?? 0) > 0;
|
|
4612
4736
|
const deadline = Date.now() + drainMs;
|
|
4613
|
-
while (
|
|
4737
|
+
while (busy() && Date.now() < deadline) {
|
|
4614
4738
|
await Bun.sleep(DRAIN_POLL_MS);
|
|
4615
4739
|
}
|
|
4616
|
-
server.stop(
|
|
4740
|
+
server.stop(busy()); // force-close iff stragglers remain past the deadline
|
|
4617
4741
|
if (this.stopHooks.length === 0)
|
|
4618
4742
|
return;
|
|
4619
4743
|
const callbacks = this.stopHooks.map((hook) => Promise.resolve().then(hook));
|
|
@@ -4630,6 +4754,7 @@ export class Server {
|
|
|
4630
4754
|
const onSignal = () => {
|
|
4631
4755
|
void this.stop(drain);
|
|
4632
4756
|
};
|
|
4757
|
+
this.signalHandler = onSignal;
|
|
4633
4758
|
process.once("SIGTERM", onSignal);
|
|
4634
4759
|
process.once("SIGINT", onSignal);
|
|
4635
4760
|
}
|