@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/src/server/server.ts
CHANGED
|
@@ -53,7 +53,11 @@ import type { ProgramValidationKind, RouteProgramStage } from "../internal/route
|
|
|
53
53
|
import { compileRouteProgram, executeRouteProgram } from "../internal/route-program.ts"
|
|
54
54
|
import { isSameOriginRequest } from "../internal/same-origin.ts"
|
|
55
55
|
import type { ResponseObserverPlugin } from "../response-observer.ts"
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
decodeRouteParams,
|
|
58
|
+
expandOptionalParams,
|
|
59
|
+
type RoutePatternSegment,
|
|
60
|
+
} from "../router/pattern.ts"
|
|
57
61
|
import { EMPTY_PARAMS, METHODS, type Method, Router } from "../router/router.ts"
|
|
58
62
|
import type {
|
|
59
63
|
InferOutput,
|
|
@@ -117,8 +121,11 @@ import { needsResolving, resolveDotSegments } from "./request-target.ts"
|
|
|
117
121
|
import {
|
|
118
122
|
applyStaticResponseHeaders,
|
|
119
123
|
buildStaticResponseHeaders,
|
|
124
|
+
claimHandedOver,
|
|
120
125
|
fusedRespond,
|
|
121
126
|
fusedRespondNoSet,
|
|
127
|
+
handedBackResponse,
|
|
128
|
+
recordResidentTexts,
|
|
122
129
|
toResponse,
|
|
123
130
|
writableResponse,
|
|
124
131
|
} from "./respond.ts"
|
|
@@ -403,6 +410,9 @@ function bunPeerPlatform(server: BunPeerServer, req: Request): Platform {
|
|
|
403
410
|
|
|
404
411
|
type BunNativeHandler = (request: Request, server: BunPeerServer) => MaybePromise<Response>
|
|
405
412
|
type BunNativeMethodTable = Partial<Record<Method, BunNativeHandler>>
|
|
413
|
+
// Bun's route table has a slot for each of these; a QUERY entry is ignored and the request falls
|
|
414
|
+
// through to the table's fetch, so QUERY routes stay on the portable dispatcher.
|
|
415
|
+
const BUN_TABLE_METHODS: readonly Method[] = METHODS.filter((method) => method !== "QUERY")
|
|
406
416
|
type BunNativeRoutes = Record<string, BunNativeMethodTable>
|
|
407
417
|
type BunRequestWithParams = Request & { readonly params?: Record<string, string> }
|
|
408
418
|
|
|
@@ -431,6 +441,21 @@ function outranks(
|
|
|
431
441
|
return ahead && other.length === route.length
|
|
432
442
|
}
|
|
433
443
|
|
|
444
|
+
/**
|
|
445
|
+
* The key Bun's route table holds `path` under. Bun refuses one with a character outside printable
|
|
446
|
+
* ASCII, so such a path is spelled as the portable router matches it: percent-encoded static
|
|
447
|
+
* segments and `:name` parameters. `segments` holds static and `:param` segments only.
|
|
448
|
+
*/
|
|
449
|
+
function bunTablePath(path: string, segments: readonly RoutePatternSegment[]): string {
|
|
450
|
+
if (!/[^ -~]/.test(path)) return path
|
|
451
|
+
let key = ""
|
|
452
|
+
for (const segment of segments) {
|
|
453
|
+
if (segment.kind === "static") key += `/${segment.value}`
|
|
454
|
+
else if (segment.kind === "param") key += `/:${segment.name}`
|
|
455
|
+
}
|
|
456
|
+
return key
|
|
457
|
+
}
|
|
458
|
+
|
|
434
459
|
const WS_PASS: WebSocketUpgradeOutcome = { kind: "pass" }
|
|
435
460
|
|
|
436
461
|
const DEFAULT_WS_UPGRADE_TIMEOUT_MS = 10_000
|
|
@@ -620,8 +645,9 @@ const withResolvedTarget = (req: Request): Request => new Request(resolveDotSegm
|
|
|
620
645
|
// request for `/public` into one for `/admin`. Node refuses such a Host before routing; so does this.
|
|
621
646
|
const HOST_AUTHORITY = /^(?:\[[0-9a-f:.%]+\]|[\w.-]+)(?::\d{1,5})?$/i
|
|
622
647
|
|
|
648
|
+
// `headerOf`, not `req.headers`: on Deno that reads the Host alone instead of building every header.
|
|
623
649
|
const invalidHost = (req: Request): boolean => {
|
|
624
|
-
const host = req
|
|
650
|
+
const host = headerOf(req, "host")
|
|
625
651
|
return host !== null && !HOST_AUTHORITY.test(host)
|
|
626
652
|
}
|
|
627
653
|
|
|
@@ -795,6 +821,16 @@ const SERVER_RUNTIMES = [
|
|
|
795
821
|
const IDENTITY_RESPONSE = (response: Response | ResponseResult): Response => webResponseOf(response)
|
|
796
822
|
const RESPONSE_TIMEOUT = (): Response => jsonError(503, "request_timeout")
|
|
797
823
|
|
|
824
|
+
/** An `onRequest` hook's answer, held to the single-use check a handler's result gets. */
|
|
825
|
+
function earlyAnswer<T>(
|
|
826
|
+
answer: Response | ResponseResult,
|
|
827
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
828
|
+
): T {
|
|
829
|
+
return answer instanceof Response && !claimHandedOver(answer)
|
|
830
|
+
? wrapResponse(plainError(500, "internal_error"))
|
|
831
|
+
: wrapResponse(answer)
|
|
832
|
+
}
|
|
833
|
+
|
|
798
834
|
// The unused-order-scoped-hook audit (recorder, seal check, stack capture, message) is a dev-time
|
|
799
835
|
// diagnostic. Every guard below keeps the INLINE literal `process.env.NODE_ENV !== "production"` at the
|
|
800
836
|
// branch - never a shared const holding the folded boolean - because a bundler's `process.env.NODE_ENV`
|
|
@@ -1155,6 +1191,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1155
1191
|
/** App-wide validation-error fallback; a route's own `schema.onValidationError` takes precedence. */
|
|
1156
1192
|
private readonly defaultOnValidationError?: RouteSchema["onValidationError"]
|
|
1157
1193
|
private bunServer: RunningServer | undefined
|
|
1194
|
+
// Bun's `pendingRequests` leaves out upgraded sockets, so `stop()` closes these itself.
|
|
1195
|
+
private bunSockets: Set<{ close(code?: number): void }> | undefined
|
|
1196
|
+
private signalHandler: (() => void) | undefined
|
|
1158
1197
|
private sealed: boolean
|
|
1159
1198
|
private readonly derives: RawDerive[]
|
|
1160
1199
|
/** Dedicated authentication stages captured by subsequent routes. */
|
|
@@ -1540,7 +1579,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1540
1579
|
/**
|
|
1541
1580
|
* Run before routing on the raw request. Return a `Response` or a `status()` result to
|
|
1542
1581
|
* short-circuit, or a replacement `Request` to continue routing with a rewritten method/URL/headers.
|
|
1543
|
-
* Global.
|
|
1582
|
+
* Global. A throw or rejection goes to every registered {@link onError} (route-less context), then
|
|
1583
|
+
* the logged default 500.
|
|
1544
1584
|
*/
|
|
1545
1585
|
onRequest(
|
|
1546
1586
|
fn: (req: Request, platform?: Platform<EnvOf<Ctx>>) => MaybePromise<OnRequestResult>,
|
|
@@ -1644,8 +1684,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1644
1684
|
|
|
1645
1685
|
/** Handle a thrown error; a non-`undefined` return becomes the response (else the default 500, or 503
|
|
1646
1686
|
* `auth_unavailable` for a failed {@link authenticate} stage). Order-scoped: captured by routes
|
|
1647
|
-
* declared *after* this call, none before; one added after the last route reaches
|
|
1648
|
-
* {@link ServerOptions.unusedScopedHooks}). */
|
|
1687
|
+
* declared *after* this call, none before; one added after the last route reaches no route (see
|
|
1688
|
+
* {@link ServerOptions.unusedScopedHooks}). Every one answers an {@link onRequest} failure. */
|
|
1649
1689
|
onError<H extends (error: unknown, context: Context & Ctx) => MaybePromise<unknown>>(
|
|
1650
1690
|
fn: H,
|
|
1651
1691
|
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
@@ -1750,6 +1790,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1750
1790
|
if (this.responseBodyTag === undefined) {
|
|
1751
1791
|
this.responseBodyTag = Object.freeze({})
|
|
1752
1792
|
this.responseBodyOwners.add(this.responseBodyTag)
|
|
1793
|
+
recordResidentTexts()
|
|
1753
1794
|
}
|
|
1754
1795
|
return this.responseBodyTag
|
|
1755
1796
|
}
|
|
@@ -2641,6 +2682,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2641
2682
|
if (this.responseBodyTag === undefined) {
|
|
2642
2683
|
this.responseBodyTag = Object.freeze({})
|
|
2643
2684
|
this.responseBodyOwners.add(this.responseBodyTag)
|
|
2685
|
+
recordResidentTexts()
|
|
2644
2686
|
}
|
|
2645
2687
|
return this.responseBodyTag
|
|
2646
2688
|
},
|
|
@@ -3322,6 +3364,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3322
3364
|
* a path whose registered route rejects this method.
|
|
3323
3365
|
*/
|
|
3324
3366
|
[RESOLVE_NODE_MOUNT](source: RequestSource): NativeMountSelection | undefined {
|
|
3367
|
+
// Node asks before every dispatch; an app with no mounts cannot select a native handler.
|
|
3368
|
+
if (this.fetchMounts.length === 0) return undefined
|
|
3325
3369
|
if (
|
|
3326
3370
|
this.onRequestHooks.length > 0 ||
|
|
3327
3371
|
this.onResponseHooks.length > 0 ||
|
|
@@ -3670,10 +3714,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3670
3714
|
req: Request,
|
|
3671
3715
|
platform?: Platform<EnvOf<Ctx>>,
|
|
3672
3716
|
): MaybePromise<WebSocketUpgradeOutcome> {
|
|
3673
|
-
// Before any header is read
|
|
3674
|
-
// costs
|
|
3717
|
+
// Before any header is read, and then through `headerOf`: Deno builds a request's whole header
|
|
3718
|
+
// list when `headers` is read, which costs every plain request of an app with sockets measurably.
|
|
3675
3719
|
if (this.wsRoutes.length === 0 && !this.mountsTakeUpgrades()) return WS_PASS
|
|
3676
|
-
if (req
|
|
3720
|
+
if (headerOf(req, "upgrade")?.toLowerCase() !== "websocket") return WS_PASS
|
|
3677
3721
|
// A WebSocket handshake is a GET (RFC 6455 section 4.1). Any other method with an Upgrade header
|
|
3678
3722
|
// is an ordinary request: its HTTP route answers it, and normal routing refuses an unknown token.
|
|
3679
3723
|
if (req.method !== "GET") return WS_PASS
|
|
@@ -3847,18 +3891,31 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3847
3891
|
this.wsHookedPasses.set(received, request)
|
|
3848
3892
|
return resolveWithDeadline(request)
|
|
3849
3893
|
}
|
|
3894
|
+
// The same boundary as an HTTP request's hooks; the answer is already wrapped, so it is not refused.
|
|
3895
|
+
const failed = (err: unknown): MaybePromise<WebSocketUpgradeOutcome> => {
|
|
3896
|
+
const answer = this.onRequestFailure(
|
|
3897
|
+
err,
|
|
3898
|
+
req,
|
|
3899
|
+
platform,
|
|
3900
|
+
this.finalizeResponse,
|
|
3901
|
+
this.wrapWebResponse,
|
|
3902
|
+
)
|
|
3903
|
+
return answer instanceof Promise
|
|
3904
|
+
? answer.then((response) => ({ kind: "reject", response }))
|
|
3905
|
+
: { kind: "reject", response: answer }
|
|
3906
|
+
}
|
|
3850
3907
|
let hooked: MaybePromise<Request | Response>
|
|
3851
3908
|
try {
|
|
3852
3909
|
hooked = this.runWebSocketRequestHooks(req, platform)
|
|
3853
|
-
} catch {
|
|
3854
|
-
return
|
|
3910
|
+
} catch (err) {
|
|
3911
|
+
return failed(err)
|
|
3855
3912
|
}
|
|
3856
3913
|
if (!(hooked instanceof Promise)) {
|
|
3857
3914
|
return hooked instanceof Response ? refuse(hooked) : resolveHooked(hooked)
|
|
3858
3915
|
}
|
|
3859
3916
|
const pending = hooked.then(
|
|
3860
3917
|
(result) => (result instanceof Response ? refuse(result) : resolveHooked(result)),
|
|
3861
|
-
|
|
3918
|
+
failed,
|
|
3862
3919
|
)
|
|
3863
3920
|
return bounded(
|
|
3864
3921
|
Promise.resolve(pending).then((result) => result),
|
|
@@ -3967,22 +4024,29 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3967
4024
|
const request = this.nodeRequestContextOf(source)
|
|
3968
4025
|
for (let i = 0; i < hooks.length; i++) {
|
|
3969
4026
|
const hook = hooks[i] as NodeRequestHook
|
|
3970
|
-
|
|
4027
|
+
let outcome: MaybePromise<Response | undefined>
|
|
4028
|
+
try {
|
|
4029
|
+
outcome = hook(request, platform)
|
|
4030
|
+
} catch (err) {
|
|
4031
|
+
return this.onRequestFailure(err, source, platform, finalize, wrapResponse)
|
|
4032
|
+
}
|
|
3971
4033
|
if (outcome instanceof Promise) {
|
|
3972
|
-
return outcome.then(
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
4034
|
+
return outcome.then(
|
|
4035
|
+
(early) =>
|
|
4036
|
+
this.continueNodeRequest(
|
|
4037
|
+
early,
|
|
4038
|
+
i + 1,
|
|
4039
|
+
request,
|
|
4040
|
+
source,
|
|
4041
|
+
platform,
|
|
4042
|
+
finalize,
|
|
4043
|
+
wrapResponse,
|
|
4044
|
+
onTimeout,
|
|
4045
|
+
),
|
|
4046
|
+
(err) => this.onRequestFailure(err, source, platform, finalize, wrapResponse),
|
|
3983
4047
|
)
|
|
3984
4048
|
}
|
|
3985
|
-
if (outcome !== undefined) return
|
|
4049
|
+
if (outcome !== undefined) return earlyAnswer(outcome, wrapResponse)
|
|
3986
4050
|
}
|
|
3987
4051
|
return this.routeAndRun(source, platform, finalize, wrapResponse, onTimeout, false)
|
|
3988
4052
|
}
|
|
@@ -3997,12 +4061,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3997
4061
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3998
4062
|
onTimeout: () => T,
|
|
3999
4063
|
): Promise<T> {
|
|
4000
|
-
if (first !== undefined) return
|
|
4064
|
+
if (first !== undefined) return earlyAnswer(first, wrapResponse)
|
|
4001
4065
|
for (let i = nextIndex; i < this.onNodeRequestHooks.length; i++) {
|
|
4002
4066
|
const hook = this.onNodeRequestHooks[i] as NodeRequestHook
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4067
|
+
let early: Response | undefined
|
|
4068
|
+
try {
|
|
4069
|
+
const outcome = hook(request, platform)
|
|
4070
|
+
early = outcome instanceof Promise ? await outcome : outcome
|
|
4071
|
+
} catch (err) {
|
|
4072
|
+
return this.onRequestFailure(err, source, platform, finalize, wrapResponse)
|
|
4073
|
+
}
|
|
4074
|
+
if (early !== undefined) return earlyAnswer(early, wrapResponse)
|
|
4006
4075
|
}
|
|
4007
4076
|
return this.routeAndRun(source, platform, finalize, wrapResponse, onTimeout, false)
|
|
4008
4077
|
}
|
|
@@ -4035,20 +4104,27 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4035
4104
|
let current: RequestSource = source
|
|
4036
4105
|
for (let i = 0; i < hooks.length; i++) {
|
|
4037
4106
|
const seen = requestOf(current)
|
|
4038
|
-
|
|
4107
|
+
let outcome: MaybePromise<OnRequestResult>
|
|
4108
|
+
try {
|
|
4109
|
+
outcome = (hooks[i] as RawOnRequest)(seen, platform)
|
|
4110
|
+
} catch (err) {
|
|
4111
|
+
return this.onRequestFailure(err, current, platform, finalize, wrapResponse)
|
|
4112
|
+
}
|
|
4039
4113
|
if (outcome instanceof Promise) {
|
|
4040
|
-
return outcome.then(
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4114
|
+
return outcome.then(
|
|
4115
|
+
(early) =>
|
|
4116
|
+
this.continueOnRequest(
|
|
4117
|
+
early,
|
|
4118
|
+
i + 1,
|
|
4119
|
+
originalRequest,
|
|
4120
|
+
current,
|
|
4121
|
+
platform,
|
|
4122
|
+
finalize,
|
|
4123
|
+
wrapResponse,
|
|
4124
|
+
onTimeout,
|
|
4125
|
+
webFast,
|
|
4126
|
+
),
|
|
4127
|
+
(err) => this.onRequestFailure(err, current, platform, finalize, wrapResponse),
|
|
4052
4128
|
)
|
|
4053
4129
|
}
|
|
4054
4130
|
if (outcome instanceof Request) {
|
|
@@ -4057,7 +4133,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4057
4133
|
if (outcome !== originalRequest) this.responseRequests.set(originalRequest, outcome)
|
|
4058
4134
|
continue
|
|
4059
4135
|
}
|
|
4060
|
-
if (outcome !== undefined) return
|
|
4136
|
+
if (outcome !== undefined) return earlyAnswer(outcome, wrapResponse)
|
|
4061
4137
|
}
|
|
4062
4138
|
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast)
|
|
4063
4139
|
}
|
|
@@ -4085,12 +4161,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4085
4161
|
current = early
|
|
4086
4162
|
if (early !== originalRequest) this.responseRequests.set(originalRequest, early)
|
|
4087
4163
|
} else if (early !== undefined) {
|
|
4088
|
-
return
|
|
4164
|
+
return earlyAnswer(early, wrapResponse)
|
|
4089
4165
|
}
|
|
4090
4166
|
if (index >= this.onRequestHooks.length) break
|
|
4091
4167
|
seen = requestOf(current)
|
|
4092
|
-
|
|
4093
|
-
|
|
4168
|
+
try {
|
|
4169
|
+
const outcome = (this.onRequestHooks[index] as RawOnRequest)(seen, platform)
|
|
4170
|
+
early = outcome instanceof Promise ? await outcome : outcome
|
|
4171
|
+
} catch (err) {
|
|
4172
|
+
return this.onRequestFailure(err, current, platform, finalize, wrapResponse)
|
|
4173
|
+
}
|
|
4094
4174
|
index++
|
|
4095
4175
|
}
|
|
4096
4176
|
return this.routeAndRun(current, platform, finalize, wrapResponse, onTimeout, webFast)
|
|
@@ -4342,34 +4422,26 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4342
4422
|
const method = source.method
|
|
4343
4423
|
if (method !== "GET" && method !== "HEAD") markTransportCap(source, entry.bodyLimit)
|
|
4344
4424
|
}
|
|
4345
|
-
// An idempotency route
|
|
4346
|
-
//
|
|
4425
|
+
// An idempotency route meets its dedupe gate inside the program, so the gate runs after
|
|
4426
|
+
// authentication and under the request deadline. Its lanes render a `Response`, which the gate
|
|
4427
|
+
// stores. Non-idempotent routes skip straight to the lanes - no added cost.
|
|
4347
4428
|
// The runtime is always present when a route resolved idempotency (enforced at registration).
|
|
4348
4429
|
if (entry.idempotent !== undefined && this.idempotencyRuntime !== undefined) {
|
|
4349
|
-
return this.idempotencyRuntime
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
prm,
|
|
4365
|
-
srch,
|
|
4366
|
-
this.finalizeResponse,
|
|
4367
|
-
this.wrapWebResponse,
|
|
4368
|
-
this.webResponseTimeout,
|
|
4369
|
-
false,
|
|
4370
|
-
),
|
|
4371
|
-
},
|
|
4372
|
-
)
|
|
4430
|
+
return this.idempotencyRuntime
|
|
4431
|
+
.run(source, platform, (prepared) =>
|
|
4432
|
+
this.runMatchedLanes(
|
|
4433
|
+
prepared,
|
|
4434
|
+
platform,
|
|
4435
|
+
entry,
|
|
4436
|
+
params,
|
|
4437
|
+
search,
|
|
4438
|
+
this.finalizeResponse,
|
|
4439
|
+
this.wrapWebResponse,
|
|
4440
|
+
this.webResponseTimeout,
|
|
4441
|
+
false,
|
|
4442
|
+
),
|
|
4443
|
+
)
|
|
4444
|
+
.then(wrapResponse)
|
|
4373
4445
|
}
|
|
4374
4446
|
return this.runMatchedLanes(
|
|
4375
4447
|
source,
|
|
@@ -6171,7 +6243,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6171
6243
|
const handed = current
|
|
6172
6244
|
return next.then((settled) => this.continueOnResponse(settled, i + 1, req, handed))
|
|
6173
6245
|
}
|
|
6174
|
-
current = next === current ? next :
|
|
6246
|
+
current = next === current ? next : handedBackResponse(next, req)
|
|
6175
6247
|
}
|
|
6176
6248
|
return current
|
|
6177
6249
|
}
|
|
@@ -6247,11 +6319,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6247
6319
|
req: Request,
|
|
6248
6320
|
handed: Response,
|
|
6249
6321
|
): Promise<Response> {
|
|
6250
|
-
let current = response === handed ? response :
|
|
6322
|
+
let current = response === handed ? response : handedBackResponse(response, req)
|
|
6251
6323
|
for (let i = nextIndex; i < this.onResponseHooks.length; i++) {
|
|
6252
6324
|
const next = (this.onResponseHooks[i] as RawOnResponse)(current, req)
|
|
6253
6325
|
const settled = next instanceof Promise ? await next : next
|
|
6254
|
-
current = settled === current ? settled :
|
|
6326
|
+
current = settled === current ? settled : handedBackResponse(settled, req)
|
|
6255
6327
|
}
|
|
6256
6328
|
return current
|
|
6257
6329
|
}
|
|
@@ -6361,13 +6433,52 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6361
6433
|
this.logRequestError(err, ctx)
|
|
6362
6434
|
return fallback === undefined ? wrapResponse(plainError(500, "internal_error")) : fallback()
|
|
6363
6435
|
}
|
|
6364
|
-
return this.runErrorHooks(
|
|
6436
|
+
return this.runErrorHooks(
|
|
6437
|
+
entry.onError,
|
|
6438
|
+
entry.responseContract,
|
|
6439
|
+
err,
|
|
6440
|
+
ctx,
|
|
6441
|
+
finalize,
|
|
6442
|
+
wrapResponse,
|
|
6443
|
+
fallback,
|
|
6444
|
+
)
|
|
6445
|
+
}
|
|
6446
|
+
|
|
6447
|
+
/** An `onRequest` hook threw or rejected. No route has matched, so every `onError` the app holds
|
|
6448
|
+
* answers it, with a route-less context; otherwise the logged default 500, as for a handler. */
|
|
6449
|
+
private onRequestFailure<T>(
|
|
6450
|
+
err: unknown,
|
|
6451
|
+
source: RequestSource,
|
|
6452
|
+
platform: Platform | undefined,
|
|
6453
|
+
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
6454
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6455
|
+
): MaybePromise<T> {
|
|
6456
|
+
if (err instanceof Response || isResponseResult(err)) return wrapResponse(err)
|
|
6457
|
+
const ctx = new RequestContext(
|
|
6458
|
+
source,
|
|
6459
|
+
EMPTY_PARAMS,
|
|
6460
|
+
undefined,
|
|
6461
|
+
getNeverAbortSignal(),
|
|
6462
|
+
getUnboundedRequestBudget(),
|
|
6463
|
+
platform,
|
|
6464
|
+
this.maxBodyBytes,
|
|
6465
|
+
this.protoPoisoning,
|
|
6466
|
+
)
|
|
6467
|
+
if (this.onErrorHooks.length === 0) {
|
|
6468
|
+
this.logRequestError(err, ctx)
|
|
6469
|
+
return this.internalErrorResponse(wrapResponse)
|
|
6470
|
+
}
|
|
6471
|
+
Object.assign(ctx, this.decorations)
|
|
6472
|
+
return this.runErrorHooks(this.onErrorHooks, undefined, err, ctx, finalize, wrapResponse)
|
|
6365
6473
|
}
|
|
6366
6474
|
|
|
6367
6475
|
/** The `onError` chain, split off {@link handleLifecycleError} so its `await` costs only the routes
|
|
6368
|
-
* that registered a hook. A hook may return a custom response; otherwise the default 500 stands.
|
|
6476
|
+
* that registered a hook. A hook may return a custom response; otherwise the default 500 stands.
|
|
6477
|
+
* Never rejects: nothing outside it re-enters `onError`, so a hook that throws, or an answer that
|
|
6478
|
+
* cannot be rendered, is logged and answered here. */
|
|
6369
6479
|
private async runErrorHooks<T>(
|
|
6370
|
-
|
|
6480
|
+
hooks: readonly RawErrorHandler[],
|
|
6481
|
+
contract: RouteEntry["responseContract"],
|
|
6371
6482
|
err: unknown,
|
|
6372
6483
|
ctx: RawContext,
|
|
6373
6484
|
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
@@ -6376,19 +6487,32 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6376
6487
|
): Promise<T> {
|
|
6377
6488
|
// Indexed for the same reason as the `beforeHandle` chain above: the iterator would outlive
|
|
6378
6489
|
// the `await` and stay heap-allocated on both engines.
|
|
6379
|
-
for (let i = 0; i <
|
|
6380
|
-
|
|
6381
|
-
|
|
6490
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
6491
|
+
let handled: unknown
|
|
6492
|
+
try {
|
|
6493
|
+
const outcome = hooks[i]!(err, ctx)
|
|
6494
|
+
handled = outcome instanceof Promise ? await outcome : outcome
|
|
6495
|
+
} catch (hookError) {
|
|
6496
|
+
// A thrown Response or status() is the hook's answer, as from beforeHandle.
|
|
6497
|
+
if (!(hookError instanceof Response) && !isResponseResult(hookError)) {
|
|
6498
|
+
this.logRequestError(err, ctx)
|
|
6499
|
+
this.logRequestError(hookError, ctx)
|
|
6500
|
+
return fallback === undefined
|
|
6501
|
+
? wrapResponse(plainError(500, "internal_error"))
|
|
6502
|
+
: fallback()
|
|
6503
|
+
}
|
|
6504
|
+
handled = hookError
|
|
6505
|
+
}
|
|
6382
6506
|
if (handled === undefined) continue
|
|
6383
|
-
|
|
6384
|
-
if (contract === undefined || (handled !== ctx.jsonReply && !isResponseResult(handled)))
|
|
6385
|
-
return finalize(handled, responseSet(ctx), ctx)
|
|
6386
|
-
// Checked here, not through finishLifecycleContract: a throwing schema must not re-enter onError.
|
|
6507
|
+
// Awaited so an asynchronous finalize's rejection lands in the catch too.
|
|
6387
6508
|
try {
|
|
6509
|
+
if (contract === undefined || (handled !== ctx.jsonReply && !isResponseResult(handled)))
|
|
6510
|
+
return await finalize(handled, responseSet(ctx), ctx)
|
|
6511
|
+
// Checked here, not through finishLifecycleContract: a throwing schema must not re-enter onError.
|
|
6388
6512
|
const checked = await contract.runtime.check(contract.definition, handled, ctx)
|
|
6389
|
-
return this.finishContractOutcome(ctx, finalize, wrapResponse, checked)
|
|
6390
|
-
} catch (
|
|
6391
|
-
this.logRequestError(
|
|
6513
|
+
return await this.finishContractOutcome(ctx, finalize, wrapResponse, checked)
|
|
6514
|
+
} catch (finishError) {
|
|
6515
|
+
this.logRequestError(finishError, ctx)
|
|
6392
6516
|
return wrapResponse(plainError(500, "internal_error"))
|
|
6393
6517
|
}
|
|
6394
6518
|
}
|
|
@@ -6657,9 +6781,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6657
6781
|
// Where Bun could pass a request down: the methods served natively on a path with a parameter,
|
|
6658
6782
|
// as segment count + method. Two table paths match one request only at the same segment count.
|
|
6659
6783
|
const below = new Set<string>()
|
|
6660
|
-
const tableAt = (
|
|
6661
|
-
|
|
6662
|
-
|
|
6784
|
+
const tableAt = (
|
|
6785
|
+
path: string,
|
|
6786
|
+
segments: readonly RoutePatternSegment[],
|
|
6787
|
+
): BunNativeMethodTable => {
|
|
6788
|
+
const key = bunTablePath(path, segments)
|
|
6789
|
+
routes[key] ??= Object.create(null) as BunNativeMethodTable
|
|
6790
|
+
return routes[key]
|
|
6663
6791
|
}
|
|
6664
6792
|
for (let i = 0; i < all.length; i++) {
|
|
6665
6793
|
const { method, path, pattern, entry } = all[i]!
|
|
@@ -6672,7 +6800,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6672
6800
|
if (
|
|
6673
6801
|
run === undefined ||
|
|
6674
6802
|
entry.idempotent !== undefined ||
|
|
6675
|
-
!
|
|
6803
|
+
!BUN_TABLE_METHODS.includes(method as Method) ||
|
|
6676
6804
|
(preflightOnFallback && method === "OPTIONS") ||
|
|
6677
6805
|
under.get(run)?.some((other) => outranks(other.pattern.segments, pattern.segments))
|
|
6678
6806
|
) {
|
|
@@ -6680,7 +6808,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6680
6808
|
}
|
|
6681
6809
|
const paramNames = pattern.paramNames
|
|
6682
6810
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined
|
|
6683
|
-
tableAt(path)[method as Method] = this.compileBunNativeHandler(
|
|
6811
|
+
tableAt(path, pattern.segments)[method as Method] = this.compileBunNativeHandler(
|
|
6684
6812
|
entry,
|
|
6685
6813
|
paramNames,
|
|
6686
6814
|
fused,
|
|
@@ -6711,8 +6839,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6711
6839
|
for (let i = 0; i < all.length; i++) {
|
|
6712
6840
|
if (lead[i] === undefined) continue
|
|
6713
6841
|
const { path, pattern } = all[i]!
|
|
6714
|
-
for (const method of
|
|
6715
|
-
if (below.has(pattern.segments.length + method))
|
|
6842
|
+
for (const method of BUN_TABLE_METHODS) {
|
|
6843
|
+
if (below.has(pattern.segments.length + method))
|
|
6844
|
+
tableAt(path, pattern.segments)[method] ??= fallback
|
|
6716
6845
|
}
|
|
6717
6846
|
}
|
|
6718
6847
|
return routes
|
|
@@ -6831,6 +6960,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6831
6960
|
}
|
|
6832
6961
|
const nativeRoutes =
|
|
6833
6962
|
wsHandlers === undefined ? this.buildBunNativeRoutes(fallback, peer) : undefined
|
|
6963
|
+
const sockets = new Set<{ close(code?: number): void }>()
|
|
6964
|
+
this.bunSockets = sockets
|
|
6834
6965
|
const running = (wsHandlers === undefined
|
|
6835
6966
|
? Bun.serve({
|
|
6836
6967
|
...listening,
|
|
@@ -6848,9 +6979,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6848
6979
|
// closes an over-cap connection before the handler runs. Default 1 MB (maxBodyBytes). One
|
|
6849
6980
|
// limit per listener: the largest cap among this app and the apps mounted on it.
|
|
6850
6981
|
maxPayloadLength: wsHandlers.maxPayloadLength,
|
|
6851
|
-
open: (ws) =>
|
|
6982
|
+
open: (ws) => {
|
|
6983
|
+
sockets.add(ws)
|
|
6984
|
+
wsHandlers.open(ws)
|
|
6985
|
+
// An upgrade accepted while `stop()` drains gets the close the earlier sockets got.
|
|
6986
|
+
if (this.bunServer !== running) ws.close(1001)
|
|
6987
|
+
},
|
|
6852
6988
|
message: (ws, message) => wsHandlers.message(ws, message),
|
|
6853
|
-
close: (ws, code, reason) =>
|
|
6989
|
+
close: (ws, code, reason) => {
|
|
6990
|
+
sockets.delete(ws)
|
|
6991
|
+
wsHandlers.close(ws, code, reason)
|
|
6992
|
+
},
|
|
6854
6993
|
},
|
|
6855
6994
|
})) as unknown as RunningServer
|
|
6856
6995
|
this.bunServer = running
|
|
@@ -6878,17 +7017,27 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6878
7017
|
* `stop()`'s promise drops in-flight requests), and decide graceful-vs-forced in
|
|
6879
7018
|
* ONE call (Bun can't escalate an already-graceful `stop()` to a forced close).
|
|
6880
7019
|
* New connections may be accepted during the drain window; in a real deploy the
|
|
6881
|
-
* load balancer has already stopped routing here, and `drainMs` bounds it.
|
|
7020
|
+
* load balancer has already stopped routing here, and `drainMs` bounds it. Open WebSockets
|
|
7021
|
+
* get a 1001 close at once and the same window to finish it, before any `onStop` hook runs.
|
|
6882
7022
|
*/
|
|
6883
7023
|
async stop({ drainMs = DEFAULT_DRAIN_MS }: { drainMs?: number } = {}): Promise<void> {
|
|
7024
|
+
const onSignal = this.signalHandler
|
|
7025
|
+
if (onSignal !== undefined) {
|
|
7026
|
+
this.signalHandler = undefined
|
|
7027
|
+
process.off("SIGTERM", onSignal).off("SIGINT", onSignal)
|
|
7028
|
+
}
|
|
6884
7029
|
const server = this.bunServer
|
|
6885
7030
|
if (server === undefined) return
|
|
6886
7031
|
this.bunServer = undefined
|
|
7032
|
+
const sockets = this.bunSockets
|
|
7033
|
+
this.bunSockets = undefined
|
|
7034
|
+
if (sockets !== undefined) for (const ws of sockets) ws.close(1001)
|
|
7035
|
+
const busy = (): boolean => server.pendingRequests + (sockets?.size ?? 0) > 0
|
|
6887
7036
|
const deadline = Date.now() + drainMs
|
|
6888
|
-
while (
|
|
7037
|
+
while (busy() && Date.now() < deadline) {
|
|
6889
7038
|
await Bun.sleep(DRAIN_POLL_MS)
|
|
6890
7039
|
}
|
|
6891
|
-
server.stop(
|
|
7040
|
+
server.stop(busy()) // force-close iff stragglers remain past the deadline
|
|
6892
7041
|
if (this.stopHooks.length === 0) return
|
|
6893
7042
|
const callbacks = this.stopHooks.map((hook) => Promise.resolve().then(hook))
|
|
6894
7043
|
const settled = Promise.allSettled(callbacks)
|
|
@@ -6905,6 +7054,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6905
7054
|
const onSignal = (): void => {
|
|
6906
7055
|
void this.stop(drain)
|
|
6907
7056
|
}
|
|
7057
|
+
this.signalHandler = onSignal
|
|
6908
7058
|
process.once("SIGTERM", onSignal)
|
|
6909
7059
|
process.once("SIGINT", onSignal)
|
|
6910
7060
|
}
|