@nifrajs/core 5.4.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 +29 -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/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 +179 -59
- 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 +29 -6
- package/dist/static-file.d.ts.map +1 -1
- package/dist/static-file.js +43 -12
- package/dist/static-file.js.map +1 -1
- package/package.json +1 -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/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 +249 -99
- package/src/server/transport-lane.ts +28 -22
- package/src/static-file.ts +72 -16
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`
|
|
@@ -559,6 +586,9 @@ export class Server {
|
|
|
559
586
|
/** App-wide validation-error fallback; a route's own `schema.onValidationError` takes precedence. */
|
|
560
587
|
defaultOnValidationError;
|
|
561
588
|
bunServer;
|
|
589
|
+
// Bun's `pendingRequests` leaves out upgraded sockets, so `stop()` closes these itself.
|
|
590
|
+
bunSockets;
|
|
591
|
+
signalHandler;
|
|
562
592
|
sealed;
|
|
563
593
|
derives;
|
|
564
594
|
/** Dedicated authentication stages captured by subsequent routes. */
|
|
@@ -900,7 +930,8 @@ export class Server {
|
|
|
900
930
|
/**
|
|
901
931
|
* Run before routing on the raw request. Return a `Response` or a `status()` result to
|
|
902
932
|
* short-circuit, or a replacement `Request` to continue routing with a rewritten method/URL/headers.
|
|
903
|
-
* Global.
|
|
933
|
+
* Global. A throw or rejection goes to every registered {@link onError} (route-less context), then
|
|
934
|
+
* the logged default 500.
|
|
904
935
|
*/
|
|
905
936
|
onRequest(fn) {
|
|
906
937
|
this.assertConfigurable("onRequest()");
|
|
@@ -986,8 +1017,8 @@ export class Server {
|
|
|
986
1017
|
}
|
|
987
1018
|
/** Handle a thrown error; a non-`undefined` return becomes the response (else the default 500, or 503
|
|
988
1019
|
* `auth_unavailable` for a failed {@link authenticate} stage). Order-scoped: captured by routes
|
|
989
|
-
* declared *after* this call, none before; one added after the last route reaches
|
|
990
|
-
* {@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. */
|
|
991
1022
|
onError(fn) {
|
|
992
1023
|
this.assertConfigurable("onError()");
|
|
993
1024
|
this.onErrorHooks.push(fn);
|
|
@@ -1080,6 +1111,7 @@ export class Server {
|
|
|
1080
1111
|
if (this.responseBodyTag === undefined) {
|
|
1081
1112
|
this.responseBodyTag = Object.freeze({});
|
|
1082
1113
|
this.responseBodyOwners.add(this.responseBodyTag);
|
|
1114
|
+
recordResidentTexts();
|
|
1083
1115
|
}
|
|
1084
1116
|
return this.responseBodyTag;
|
|
1085
1117
|
}
|
|
@@ -1577,6 +1609,7 @@ export class Server {
|
|
|
1577
1609
|
if (this.responseBodyTag === undefined) {
|
|
1578
1610
|
this.responseBodyTag = Object.freeze({});
|
|
1579
1611
|
this.responseBodyOwners.add(this.responseBodyTag);
|
|
1612
|
+
recordResidentTexts();
|
|
1580
1613
|
}
|
|
1581
1614
|
return this.responseBodyTag;
|
|
1582
1615
|
},
|
|
@@ -2098,6 +2131,9 @@ export class Server {
|
|
|
2098
2131
|
* a path whose registered route rejects this method.
|
|
2099
2132
|
*/
|
|
2100
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;
|
|
2101
2137
|
if (this.onRequestHooks.length > 0 ||
|
|
2102
2138
|
this.onResponseHooks.length > 0 ||
|
|
2103
2139
|
this.onResponseFinalizedHooks.length > 0 ||
|
|
@@ -2386,11 +2422,11 @@ export class Server {
|
|
|
2386
2422
|
}
|
|
2387
2423
|
}
|
|
2388
2424
|
resolveWebSocketUpgrade(req, platform) {
|
|
2389
|
-
// Before any header is read
|
|
2390
|
-
// 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.
|
|
2391
2427
|
if (this.wsRoutes.length === 0 && !this.mountsTakeUpgrades())
|
|
2392
2428
|
return WS_PASS;
|
|
2393
|
-
if (req
|
|
2429
|
+
if (headerOf(req, "upgrade")?.toLowerCase() !== "websocket")
|
|
2394
2430
|
return WS_PASS;
|
|
2395
2431
|
// A WebSocket handshake is a GET (RFC 6455 section 4.1). Any other method with an Upgrade header
|
|
2396
2432
|
// is an ordinary request: its HTTP route answers it, and normal routing refuses an unknown token.
|
|
@@ -2552,17 +2588,24 @@ export class Server {
|
|
|
2552
2588
|
this.wsHookedPasses.set(received, request);
|
|
2553
2589
|
return resolveWithDeadline(request);
|
|
2554
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
|
+
};
|
|
2555
2598
|
let hooked;
|
|
2556
2599
|
try {
|
|
2557
2600
|
hooked = this.runWebSocketRequestHooks(req, platform);
|
|
2558
2601
|
}
|
|
2559
|
-
catch {
|
|
2560
|
-
return
|
|
2602
|
+
catch (err) {
|
|
2603
|
+
return failed(err);
|
|
2561
2604
|
}
|
|
2562
2605
|
if (!(hooked instanceof Promise)) {
|
|
2563
2606
|
return hooked instanceof Response ? refuse(hooked) : resolveHooked(hooked);
|
|
2564
2607
|
}
|
|
2565
|
-
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);
|
|
2566
2609
|
return bounded(Promise.resolve(pending).then((result) => result), onTimeout);
|
|
2567
2610
|
}
|
|
2568
2611
|
/** Bun `fetch` when WS routes exist: try a WS upgrade first, else run the normal HTTP lifecycle.
|
|
@@ -2637,24 +2680,36 @@ export class Server {
|
|
|
2637
2680
|
const request = this.nodeRequestContextOf(source);
|
|
2638
2681
|
for (let i = 0; i < hooks.length; i++) {
|
|
2639
2682
|
const hook = hooks[i];
|
|
2640
|
-
|
|
2683
|
+
let outcome;
|
|
2684
|
+
try {
|
|
2685
|
+
outcome = hook(request, platform);
|
|
2686
|
+
}
|
|
2687
|
+
catch (err) {
|
|
2688
|
+
return this.onRequestFailure(err, source, platform, finalize, wrapResponse);
|
|
2689
|
+
}
|
|
2641
2690
|
if (outcome instanceof Promise) {
|
|
2642
|
-
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));
|
|
2643
2692
|
}
|
|
2644
2693
|
if (outcome !== undefined)
|
|
2645
|
-
return
|
|
2694
|
+
return earlyAnswer(outcome, wrapResponse);
|
|
2646
2695
|
}
|
|
2647
2696
|
return this.routeAndRun(source, platform, finalize, wrapResponse, onTimeout, false);
|
|
2648
2697
|
}
|
|
2649
2698
|
async continueNodeRequest(first, nextIndex, request, source, platform, finalize, wrapResponse, onTimeout) {
|
|
2650
2699
|
if (first !== undefined)
|
|
2651
|
-
return
|
|
2700
|
+
return earlyAnswer(first, wrapResponse);
|
|
2652
2701
|
for (let i = nextIndex; i < this.onNodeRequestHooks.length; i++) {
|
|
2653
2702
|
const hook = this.onNodeRequestHooks[i];
|
|
2654
|
-
|
|
2655
|
-
|
|
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
|
+
}
|
|
2656
2711
|
if (early !== undefined)
|
|
2657
|
-
return
|
|
2712
|
+
return earlyAnswer(early, wrapResponse);
|
|
2658
2713
|
}
|
|
2659
2714
|
return this.routeAndRun(source, platform, finalize, wrapResponse, onTimeout, false);
|
|
2660
2715
|
}
|
|
@@ -2679,9 +2734,15 @@ export class Server {
|
|
|
2679
2734
|
let current = source;
|
|
2680
2735
|
for (let i = 0; i < hooks.length; i++) {
|
|
2681
2736
|
const seen = requestOf(current);
|
|
2682
|
-
|
|
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
|
+
}
|
|
2683
2744
|
if (outcome instanceof Promise) {
|
|
2684
|
-
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));
|
|
2685
2746
|
}
|
|
2686
2747
|
if (outcome instanceof Request) {
|
|
2687
2748
|
if (outcome !== seen)
|
|
@@ -2692,7 +2753,7 @@ export class Server {
|
|
|
2692
2753
|
continue;
|
|
2693
2754
|
}
|
|
2694
2755
|
if (outcome !== undefined)
|
|
2695
|
-
return
|
|
2756
|
+
return earlyAnswer(outcome, wrapResponse);
|
|
2696
2757
|
}
|
|
2697
2758
|
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast);
|
|
2698
2759
|
}
|
|
@@ -2712,13 +2773,18 @@ export class Server {
|
|
|
2712
2773
|
this.responseRequests.set(originalRequest, early);
|
|
2713
2774
|
}
|
|
2714
2775
|
else if (early !== undefined) {
|
|
2715
|
-
return
|
|
2776
|
+
return earlyAnswer(early, wrapResponse);
|
|
2716
2777
|
}
|
|
2717
2778
|
if (index >= this.onRequestHooks.length)
|
|
2718
2779
|
break;
|
|
2719
2780
|
seen = requestOf(current);
|
|
2720
|
-
|
|
2721
|
-
|
|
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
|
+
}
|
|
2722
2788
|
index++;
|
|
2723
2789
|
}
|
|
2724
2790
|
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast);
|
|
@@ -2933,14 +2999,14 @@ export class Server {
|
|
|
2933
2999
|
if (method !== "GET" && method !== "HEAD")
|
|
2934
3000
|
markTransportCap(source, entry.bodyLimit);
|
|
2935
3001
|
}
|
|
2936
|
-
// An idempotency route
|
|
2937
|
-
//
|
|
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.
|
|
2938
3005
|
// The runtime is always present when a route resolved idempotency (enforced at registration).
|
|
2939
3006
|
if (entry.idempotent !== undefined && this.idempotencyRuntime !== undefined) {
|
|
2940
|
-
return this.idempotencyRuntime
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
});
|
|
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);
|
|
2944
3010
|
}
|
|
2945
3011
|
return this.runMatchedLanes(source, platform, entry, params, search, finalize, wrapResponse, onTimeout, webFast);
|
|
2946
3012
|
}
|
|
@@ -4031,7 +4097,7 @@ export class Server {
|
|
|
4031
4097
|
const handed = current;
|
|
4032
4098
|
return next.then((settled) => this.continueOnResponse(settled, i + 1, req, handed));
|
|
4033
4099
|
}
|
|
4034
|
-
current = next === current ? next :
|
|
4100
|
+
current = next === current ? next : handedBackResponse(next, req);
|
|
4035
4101
|
}
|
|
4036
4102
|
return current;
|
|
4037
4103
|
}
|
|
@@ -4091,11 +4157,11 @@ export class Server {
|
|
|
4091
4157
|
}
|
|
4092
4158
|
/** Async tail of {@link applyOnResponse}: runs the remaining hooks once one has gone async. */
|
|
4093
4159
|
async continueOnResponse(response, nextIndex, req, handed) {
|
|
4094
|
-
let current = response === handed ? response :
|
|
4160
|
+
let current = response === handed ? response : handedBackResponse(response, req);
|
|
4095
4161
|
for (let i = nextIndex; i < this.onResponseHooks.length; i++) {
|
|
4096
4162
|
const next = this.onResponseHooks[i](current, req);
|
|
4097
4163
|
const settled = next instanceof Promise ? await next : next;
|
|
4098
|
-
current = settled === current ? settled :
|
|
4164
|
+
current = settled === current ? settled : handedBackResponse(settled, req);
|
|
4099
4165
|
}
|
|
4100
4166
|
return current;
|
|
4101
4167
|
}
|
|
@@ -4177,28 +4243,57 @@ export class Server {
|
|
|
4177
4243
|
this.logRequestError(err, ctx);
|
|
4178
4244
|
return fallback === undefined ? wrapResponse(plainError(500, "internal_error")) : fallback();
|
|
4179
4245
|
}
|
|
4180
|
-
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);
|
|
4181
4260
|
}
|
|
4182
4261
|
/** The `onError` chain, split off {@link handleLifecycleError} so its `await` costs only the routes
|
|
4183
|
-
* that registered a hook. A hook may return a custom response; otherwise the default 500 stands.
|
|
4184
|
-
|
|
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) {
|
|
4185
4266
|
// Indexed for the same reason as the `beforeHandle` chain above: the iterator would outlive
|
|
4186
4267
|
// the `await` and stay heap-allocated on both engines.
|
|
4187
|
-
for (let i = 0; i <
|
|
4188
|
-
|
|
4189
|
-
|
|
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
|
+
}
|
|
4190
4285
|
if (handled === undefined)
|
|
4191
4286
|
continue;
|
|
4192
|
-
|
|
4193
|
-
if (contract === undefined || (handled !== ctx.jsonReply && !isResponseResult(handled)))
|
|
4194
|
-
return finalize(handled, responseSet(ctx), ctx);
|
|
4195
|
-
// 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.
|
|
4196
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.
|
|
4197
4292
|
const checked = await contract.runtime.check(contract.definition, handled, ctx);
|
|
4198
|
-
return this.finishContractOutcome(ctx, finalize, wrapResponse, checked);
|
|
4293
|
+
return await this.finishContractOutcome(ctx, finalize, wrapResponse, checked);
|
|
4199
4294
|
}
|
|
4200
|
-
catch (
|
|
4201
|
-
this.logRequestError(
|
|
4295
|
+
catch (finishError) {
|
|
4296
|
+
this.logRequestError(finishError, ctx);
|
|
4202
4297
|
return wrapResponse(plainError(500, "internal_error"));
|
|
4203
4298
|
}
|
|
4204
4299
|
}
|
|
@@ -4413,9 +4508,10 @@ export class Server {
|
|
|
4413
4508
|
// Where Bun could pass a request down: the methods served natively on a path with a parameter,
|
|
4414
4509
|
// as segment count + method. Two table paths match one request only at the same segment count.
|
|
4415
4510
|
const below = new Set();
|
|
4416
|
-
const tableAt = (path) => {
|
|
4417
|
-
|
|
4418
|
-
|
|
4511
|
+
const tableAt = (path, segments) => {
|
|
4512
|
+
const key = bunTablePath(path, segments);
|
|
4513
|
+
routes[key] ??= Object.create(null);
|
|
4514
|
+
return routes[key];
|
|
4419
4515
|
};
|
|
4420
4516
|
for (let i = 0; i < all.length; i++) {
|
|
4421
4517
|
const { method, path, pattern, entry } = all[i];
|
|
@@ -4427,14 +4523,14 @@ export class Server {
|
|
|
4427
4523
|
// the platform the table shares could not answer `c.clientIp` there.
|
|
4428
4524
|
if (run === undefined ||
|
|
4429
4525
|
entry.idempotent !== undefined ||
|
|
4430
|
-
!
|
|
4526
|
+
!BUN_TABLE_METHODS.includes(method) ||
|
|
4431
4527
|
(preflightOnFallback && method === "OPTIONS") ||
|
|
4432
4528
|
under.get(run)?.some((other) => outranks(other.pattern.segments, pattern.segments))) {
|
|
4433
4529
|
continue;
|
|
4434
4530
|
}
|
|
4435
4531
|
const paramNames = pattern.paramNames;
|
|
4436
4532
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined;
|
|
4437
|
-
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);
|
|
4438
4534
|
count += 1;
|
|
4439
4535
|
if (paramNames.length > 0) {
|
|
4440
4536
|
below.add(pattern.segments.length + method);
|
|
@@ -4461,9 +4557,9 @@ export class Server {
|
|
|
4461
4557
|
if (lead[i] === undefined)
|
|
4462
4558
|
continue;
|
|
4463
4559
|
const { path, pattern } = all[i];
|
|
4464
|
-
for (const method of
|
|
4560
|
+
for (const method of BUN_TABLE_METHODS) {
|
|
4465
4561
|
if (below.has(pattern.segments.length + method))
|
|
4466
|
-
tableAt(path)[method] ??= fallback;
|
|
4562
|
+
tableAt(path, pattern.segments)[method] ??= fallback;
|
|
4467
4563
|
}
|
|
4468
4564
|
}
|
|
4469
4565
|
return routes;
|
|
@@ -4560,6 +4656,8 @@ export class Server {
|
|
|
4560
4656
|
[PLATFORM_PEER]: (request) => running.requestIP(request)?.address,
|
|
4561
4657
|
};
|
|
4562
4658
|
const nativeRoutes = wsHandlers === undefined ? this.buildBunNativeRoutes(fallback, peer) : undefined;
|
|
4659
|
+
const sockets = new Set();
|
|
4660
|
+
this.bunSockets = sockets;
|
|
4563
4661
|
const running = (wsHandlers === undefined
|
|
4564
4662
|
? Bun.serve({
|
|
4565
4663
|
...listening,
|
|
@@ -4577,9 +4675,18 @@ export class Server {
|
|
|
4577
4675
|
// closes an over-cap connection before the handler runs. Default 1 MB (maxBodyBytes). One
|
|
4578
4676
|
// limit per listener: the largest cap among this app and the apps mounted on it.
|
|
4579
4677
|
maxPayloadLength: wsHandlers.maxPayloadLength,
|
|
4580
|
-
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
|
+
},
|
|
4581
4685
|
message: (ws, message) => wsHandlers.message(ws, message),
|
|
4582
|
-
close: (ws, code, reason) =>
|
|
4686
|
+
close: (ws, code, reason) => {
|
|
4687
|
+
sockets.delete(ws);
|
|
4688
|
+
wsHandlers.close(ws, code, reason);
|
|
4689
|
+
},
|
|
4583
4690
|
},
|
|
4584
4691
|
}));
|
|
4585
4692
|
this.bunServer = running;
|
|
@@ -4607,18 +4714,30 @@ export class Server {
|
|
|
4607
4714
|
* `stop()`'s promise drops in-flight requests), and decide graceful-vs-forced in
|
|
4608
4715
|
* ONE call (Bun can't escalate an already-graceful `stop()` to a forced close).
|
|
4609
4716
|
* New connections may be accepted during the drain window; in a real deploy the
|
|
4610
|
-
* 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.
|
|
4611
4719
|
*/
|
|
4612
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
|
+
}
|
|
4613
4726
|
const server = this.bunServer;
|
|
4614
4727
|
if (server === undefined)
|
|
4615
4728
|
return;
|
|
4616
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;
|
|
4617
4736
|
const deadline = Date.now() + drainMs;
|
|
4618
|
-
while (
|
|
4737
|
+
while (busy() && Date.now() < deadline) {
|
|
4619
4738
|
await Bun.sleep(DRAIN_POLL_MS);
|
|
4620
4739
|
}
|
|
4621
|
-
server.stop(
|
|
4740
|
+
server.stop(busy()); // force-close iff stragglers remain past the deadline
|
|
4622
4741
|
if (this.stopHooks.length === 0)
|
|
4623
4742
|
return;
|
|
4624
4743
|
const callbacks = this.stopHooks.map((hook) => Promise.resolve().then(hook));
|
|
@@ -4635,6 +4754,7 @@ export class Server {
|
|
|
4635
4754
|
const onSignal = () => {
|
|
4636
4755
|
void this.stop(drain);
|
|
4637
4756
|
};
|
|
4757
|
+
this.signalHandler = onSignal;
|
|
4638
4758
|
process.once("SIGTERM", onSignal);
|
|
4639
4759
|
process.once("SIGINT", onSignal);
|
|
4640
4760
|
}
|