@nifrajs/core 2.14.1 → 3.1.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/README.md +4 -0
- package/dist/assurance.d.ts +16 -8
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +33 -0
- package/dist/assurance.js.map +1 -1
- package/dist/edge-kit.d.ts +27 -0
- package/dist/edge-kit.d.ts.map +1 -0
- package/dist/edge-kit.js +24 -0
- package/dist/edge-kit.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/route-execution.d.ts +11 -5
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js +2 -1
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/response-observer.d.ts +25 -0
- package/dist/response-observer.d.ts.map +1 -0
- package/dist/response-observer.js +40 -0
- package/dist/response-observer.js.map +1 -0
- package/dist/security/baseline.d.ts +68 -0
- package/dist/security/baseline.d.ts.map +1 -0
- package/dist/security/baseline.js +118 -0
- package/dist/security/baseline.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +26 -0
- package/dist/server/bare-error-lane.d.ts.map +1 -0
- package/dist/server/bare-error-lane.js +60 -0
- package/dist/server/bare-error-lane.js.map +1 -0
- package/dist/server/body.d.ts +7 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +7 -0
- package/dist/server/body.js.map +1 -1
- package/dist/server/bridge-markers.d.ts +25 -0
- package/dist/server/bridge-markers.d.ts.map +1 -0
- package/dist/server/bridge-markers.js +25 -0
- package/dist/server/bridge-markers.js.map +1 -0
- package/dist/server/context.d.ts +6 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/header-case.d.ts.map +1 -1
- package/dist/server/header-case.js +2 -1
- package/dist/server/header-case.js.map +1 -1
- package/dist/server/http.d.ts +36 -3
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +49 -3
- package/dist/server/http.js.map +1 -1
- package/dist/server/install.d.ts +6 -8
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +7 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/node-outcome-hook.d.ts +2 -1
- package/dist/server/node-outcome-hook.d.ts.map +1 -1
- package/dist/server/node-outcome-hook.js.map +1 -1
- package/dist/server/node-outcome.d.ts +17 -2
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +37 -3
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/query.d.ts +7 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +8 -3
- package/dist/server/query.js.map +1 -1
- package/dist/server/request-context.d.ts +17 -4
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +63 -29
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +19 -2
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +5 -2
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/response-observer-runtime.d.ts +20 -0
- package/dist/server/response-observer-runtime.d.ts.map +1 -0
- package/dist/server/response-observer-runtime.js +95 -0
- package/dist/server/response-observer-runtime.js.map +1 -0
- package/dist/server/runtime-core.d.ts +60 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +65 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +3 -1
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +52 -39
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +267 -271
- package/dist/server/server.js.map +1 -1
- package/dist/server/websocket.d.ts +20 -4
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +81 -11
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +1 -1
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +12 -8
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy-register.d.ts +2 -0
- package/dist/single-copy-register.d.ts.map +1 -0
- package/dist/single-copy-register.js +24 -0
- package/dist/single-copy-register.js.map +1 -0
- package/dist/single-copy.d.ts +156 -0
- package/dist/single-copy.d.ts.map +1 -0
- package/dist/single-copy.js +470 -0
- package/dist/single-copy.js.map +1 -0
- package/package.json +24 -2
- package/src/assurance.ts +58 -1
- package/src/edge-kit.ts +27 -0
- package/src/errors.ts +1 -0
- package/src/index.ts +1 -1
- package/src/internal/route-execution.ts +23 -16
- package/src/response-observer.ts +64 -0
- package/src/security/baseline.ts +172 -0
- package/src/server/bare-error-lane.ts +76 -0
- package/src/server/body.ts +8 -0
- package/src/server/bridge-markers.ts +24 -0
- package/src/server/context.ts +6 -1
- package/src/server/header-case.ts +3 -1
- package/src/server/http.ts +53 -3
- package/src/server/install.ts +14 -0
- package/src/server/node-outcome-hook.ts +2 -1
- package/src/server/node-outcome.ts +44 -3
- package/src/server/query.ts +10 -4
- package/src/server/request-context.ts +85 -33
- package/src/server/respond.ts +26 -1
- package/src/server/response-contract-lane.ts +6 -2
- package/src/server/response-observer-runtime.ts +135 -0
- package/src/server/runtime-core.ts +98 -1
- package/src/server/server-types.ts +3 -1
- package/src/server/server.ts +436 -367
- package/src/server/websocket.ts +127 -17
- package/src/server/ws-bun.ts +26 -7
- package/src/server.ts +2 -1
- package/src/single-copy-register.ts +24 -0
- package/src/single-copy.ts +524 -0
package/src/server/server.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
type RouteEntry,
|
|
42
42
|
} from "../internal/route-execution.ts"
|
|
43
43
|
import { isSameOriginRequest } from "../internal/same-origin.ts"
|
|
44
|
+
import type { ResponseObserverPlugin } from "../response-observer.ts"
|
|
44
45
|
import { compileRoutePattern, decodeRouteParams } from "../router/pattern.ts"
|
|
45
46
|
import { EMPTY_PARAMS, type Method, Router } from "../router/router.ts"
|
|
46
47
|
import type {
|
|
@@ -49,11 +50,13 @@ import type {
|
|
|
49
50
|
StandardResult,
|
|
50
51
|
StandardSchemaV1,
|
|
51
52
|
} from "../schema/standard.ts"
|
|
53
|
+
import { emitRequestErrorLog, renderBareError } from "./bare-error-lane.ts"
|
|
52
54
|
import {
|
|
53
55
|
assertByteLimit,
|
|
54
56
|
markTransportCap,
|
|
55
57
|
markTrustedBodyFraming,
|
|
56
58
|
type RawBodyReaders,
|
|
59
|
+
UNLIMITED_BODY_BYTES,
|
|
57
60
|
} from "./body.ts"
|
|
58
61
|
import { type ClientIpTrust, resolveClientIp } from "./client-ip.ts"
|
|
59
62
|
import type { Context, Platform, ResponseControls, RouteSchema } from "./context.ts"
|
|
@@ -63,19 +66,18 @@ import {
|
|
|
63
66
|
markLowercaseHeaderKeys,
|
|
64
67
|
} from "./header-case.ts"
|
|
65
68
|
import { headerObjectOf } from "./headers.ts"
|
|
66
|
-
import { jsonError, pathnameOf, type UrlParts, urlPartsOf } from "./http.ts"
|
|
69
|
+
import { jsonError, pathnameOf, plainError, type UrlParts, urlPartsOf } from "./http.ts"
|
|
67
70
|
import { type NodeServeOutcome, withStaticNodeHeaders } from "./node-outcome.ts"
|
|
68
|
-
import {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
recordHeadersView,
|
|
71
|
+
import type {
|
|
72
|
+
NodeOutcomeRuntime,
|
|
73
|
+
NodeRequestContext,
|
|
74
|
+
NodeRequestHook,
|
|
75
|
+
NodeResponseContext,
|
|
76
|
+
NodeResponseHook,
|
|
77
|
+
ResponseBodyHook,
|
|
78
|
+
ResponseBodyReplacement,
|
|
79
|
+
ResponseHeadersHook,
|
|
80
|
+
ResponseHeadersView,
|
|
79
81
|
} from "./node-outcome-hook.ts"
|
|
80
82
|
import {
|
|
81
83
|
isUrlEncodedForm,
|
|
@@ -84,21 +86,21 @@ import {
|
|
|
84
86
|
readBoundedForm,
|
|
85
87
|
searchOf,
|
|
86
88
|
} from "./query.ts"
|
|
87
|
-
import { RequestContext, readBoundedJsonSource } from "./request-context.ts"
|
|
89
|
+
import { RequestContext, readBodyFramed, readBoundedJsonSource } from "./request-context.ts"
|
|
88
90
|
import {
|
|
89
91
|
applyStaticResponseHeaders,
|
|
90
92
|
buildStaticResponseHeaders,
|
|
91
93
|
fusedRespond,
|
|
92
94
|
fusedRespondNoSet,
|
|
93
|
-
knownMutableHeaders,
|
|
94
|
-
rememberMutableHeaders,
|
|
95
|
-
taggedResponseBody,
|
|
96
95
|
toResponse,
|
|
97
96
|
} from "./respond.ts"
|
|
98
97
|
// Type-only: erased, so the kernel never pulls the lane's implementation into a bundle that does not
|
|
99
98
|
// install the plugin. The value side arrives through the symbol-keyed install seam.
|
|
100
99
|
import type { ResponseContractRuntime } from "./response-contract-lane.ts"
|
|
101
|
-
import {
|
|
100
|
+
import type {
|
|
101
|
+
ResponseObserverMethods,
|
|
102
|
+
ResponseObserverRuntime,
|
|
103
|
+
} from "./response-observer-runtime.ts"
|
|
102
104
|
import {
|
|
103
105
|
CONTEXT_SEARCH,
|
|
104
106
|
CONTEXT_SET,
|
|
@@ -107,7 +109,10 @@ import {
|
|
|
107
109
|
getUnboundedRequestBudget,
|
|
108
110
|
type HandlerResult,
|
|
109
111
|
headerOf,
|
|
112
|
+
isResponseResult,
|
|
113
|
+
type ResponseResult,
|
|
110
114
|
requestOf,
|
|
115
|
+
status,
|
|
111
116
|
} from "./runtime-core.ts"
|
|
112
117
|
import { normalizeStaticResponseHeaders, type StaticResponseHeaders } from "./static-headers.ts"
|
|
113
118
|
|
|
@@ -125,96 +130,6 @@ export type {
|
|
|
125
130
|
ResponseHeadersView,
|
|
126
131
|
}
|
|
127
132
|
|
|
128
|
-
const HEADER_MUTABILITY_PROBE = "x-nifra-header-probe"
|
|
129
|
-
const GUARDED_RESPONSE_HEADERS = new WeakSet<Headers>()
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Detect a guarded Web Headers object before invoking user code. Retrying after catching any
|
|
133
|
-
* TypeError is observably wrong: user hooks are allowed to throw TypeError themselves, and a retry
|
|
134
|
-
* runs those hooks twice. The hot path never reaches the probe: every framework-constructed
|
|
135
|
-
* Response stamps its headers as known-mutable at construction (see respond.ts), so only a
|
|
136
|
-
* handler-returned foreign `Response` - the case that can actually be guarded - pays it, once per
|
|
137
|
-
* headers object. The probe uses a valid private header name and restores a pre-existing value, so
|
|
138
|
-
* the fallback is limited to the actual immutable/guarded-header case.
|
|
139
|
-
*/
|
|
140
|
-
function hasMutableResponseHeaders(headers: Headers): boolean {
|
|
141
|
-
if (knownMutableHeaders(headers)) return true
|
|
142
|
-
if (GUARDED_RESPONSE_HEADERS.has(headers)) return false
|
|
143
|
-
let previous: string | null = null
|
|
144
|
-
try {
|
|
145
|
-
previous = headers.get(HEADER_MUTABILITY_PROBE)
|
|
146
|
-
headers.set(HEADER_MUTABILITY_PROBE, "1")
|
|
147
|
-
if (previous === null) headers.delete(HEADER_MUTABILITY_PROBE)
|
|
148
|
-
else headers.set(HEADER_MUTABILITY_PROBE, previous)
|
|
149
|
-
rememberMutableHeaders(headers)
|
|
150
|
-
return true
|
|
151
|
-
} catch {
|
|
152
|
-
try {
|
|
153
|
-
if (previous === null) headers.delete(HEADER_MUTABILITY_PROBE)
|
|
154
|
-
else headers.set(HEADER_MUTABILITY_PROBE, previous)
|
|
155
|
-
} catch {
|
|
156
|
-
// The guarded object rejected the cleanup too; the clone below is authoritative.
|
|
157
|
-
}
|
|
158
|
-
GUARDED_RESPONSE_HEADERS.add(headers)
|
|
159
|
-
return false
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Adapt a portable {@link ResponseBodyHook} into the Web `onResponse` walk. Only a Response
|
|
164
|
-
* carrying the framework-buffered body tag participates - a raw or streamed Response is skipped by
|
|
165
|
-
* contract, never drained. */
|
|
166
|
-
function webResponseBodyHook(
|
|
167
|
-
fn: ResponseBodyHook,
|
|
168
|
-
owners: ReadonlySet<object>,
|
|
169
|
-
): (response: Response, req: Request) => MaybePromise<Response> {
|
|
170
|
-
return (response, req) => {
|
|
171
|
-
const body = taggedResponseBody(response, owners)
|
|
172
|
-
if (body === undefined) return response
|
|
173
|
-
const out = fn(body, response.headers, webRequestView(req), response.status)
|
|
174
|
-
if (out instanceof Promise) return out.then((replaced) => withReplacedBody(response, replaced))
|
|
175
|
-
return withReplacedBody(response, out)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Minimal request view over a Web `Request` for portable header hooks on the Web serving paths. */
|
|
180
|
-
function webRequestView(req: Request): NodeRequestContext {
|
|
181
|
-
return { method: req.method, url: req.url, header: (name) => req.headers.get(name) }
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Adapt a portable {@link ResponseHeadersHook} into the Web `onResponse` walk: run it against the
|
|
186
|
-
* response's own `Headers` in place (no clone). A response whose headers are GUARDED (a raw
|
|
187
|
-
* `fetch()`ed Response returned by a handler) throws on the FIRST mutation, so nothing was applied
|
|
188
|
-
* yet - rerun once against a mutable copy. Async hooks should perform their mutations before their
|
|
189
|
-
* first await for the guard fallback to cover them.
|
|
190
|
-
*/
|
|
191
|
-
function webResponseHeadersHook(
|
|
192
|
-
fn: ResponseHeadersHook,
|
|
193
|
-
): (response: Response, req: Request) => MaybePromise<Response> {
|
|
194
|
-
return (response, req) => {
|
|
195
|
-
const view = webRequestView(req)
|
|
196
|
-
if (!hasMutableResponseHeaders(response.headers)) {
|
|
197
|
-
const clone = new Response(response.body, response)
|
|
198
|
-
const out = fn(clone.headers, view, clone.status)
|
|
199
|
-
return out instanceof Promise ? out.then(() => clone) : clone
|
|
200
|
-
}
|
|
201
|
-
const out = fn(response.headers, view, response.status)
|
|
202
|
-
return out instanceof Promise ? out.then(() => response) : response
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Adapt a raw-response fallback hook. Tagged framework payloads already ran through the body tier,
|
|
207
|
-
* while untagged Responses include streams, proxied fetches, and framework-generated error responses. */
|
|
208
|
-
function webResponseRawHook(
|
|
209
|
-
fn: (response: Response, req: Request) => MaybePromise<Response>,
|
|
210
|
-
owners: ReadonlySet<object>,
|
|
211
|
-
): (response: Response, req: Request) => MaybePromise<Response> {
|
|
212
|
-
return (response, req) => {
|
|
213
|
-
if (taggedResponseBody(response, owners) !== undefined) return response
|
|
214
|
-
return fn(response, req)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
133
|
import type { IdempotencyRuntime } from "./idempotency-lane.ts"
|
|
219
134
|
import {
|
|
220
135
|
INSTALL_EFFECT_LEDGER,
|
|
@@ -222,8 +137,11 @@ import {
|
|
|
222
137
|
INSTALL_MCP,
|
|
223
138
|
INSTALL_NODE_DIRECT,
|
|
224
139
|
INSTALL_RESPONSE_CONTRACT,
|
|
140
|
+
INSTALL_RESPONSE_OBSERVER,
|
|
225
141
|
INSTALL_SSE,
|
|
226
142
|
INSTALL_WS,
|
|
143
|
+
NODE_NATIVE_MOUNT,
|
|
144
|
+
RESOLVE_NODE_MOUNT,
|
|
227
145
|
} from "./install.ts"
|
|
228
146
|
import type { EffectLedgerRuntime, ResolvedEffectLedger } from "./ledger-lane.ts"
|
|
229
147
|
import { jsonLogger, type Logger } from "./logger.ts"
|
|
@@ -363,6 +281,22 @@ interface BunUpgradeServer {
|
|
|
363
281
|
|
|
364
282
|
type MountedFetchHandler = (request: Request, platform?: Platform) => MaybePromise<Response>
|
|
365
283
|
|
|
284
|
+
/** Structural native mount contract. The serving adapter owns the concrete Node request/response
|
|
285
|
+
* types; the kernel only selects a handler after proving that taking this lane cannot skip its
|
|
286
|
+
* global lifecycle. `false` means the capability was unavailable at runtime, so the adapter must
|
|
287
|
+
* retry the ordinary Web mount with the same untouched source. */
|
|
288
|
+
type NativeMountHandler = (
|
|
289
|
+
request: unknown,
|
|
290
|
+
response: unknown,
|
|
291
|
+
platform?: Platform,
|
|
292
|
+
) => MaybePromise<undefined | false>
|
|
293
|
+
|
|
294
|
+
interface NativeMountSelection {
|
|
295
|
+
readonly handler: NativeMountHandler
|
|
296
|
+
readonly path: string
|
|
297
|
+
readonly stripPrefix: boolean
|
|
298
|
+
}
|
|
299
|
+
|
|
366
300
|
interface FetchMount {
|
|
367
301
|
readonly path: string
|
|
368
302
|
readonly handler: MountedFetchHandler
|
|
@@ -432,10 +366,11 @@ function requireMcpRuntime(runtime: McpRuntime | undefined): McpRuntime {
|
|
|
432
366
|
}
|
|
433
367
|
|
|
434
368
|
/** The handler's permitted return type. When the route declares a `response` schema, the return is
|
|
435
|
-
* constrained to the contract's type (
|
|
436
|
-
* declared
|
|
369
|
+
* constrained to the contract's type (a raw `Response`, or a `status(...)` early exit - both are
|
|
370
|
+
* control flow, not the declared payload) - so the implementation can't drift from the declared
|
|
371
|
+
* contract. Without a `response` schema it's unconstrained (`HandlerResult`), exactly as before. */
|
|
437
372
|
type ResponseOf<S extends RouteSchema> = S extends { response: infer R extends StandardSchemaV1 }
|
|
438
|
-
? InferOutput<R> | Response
|
|
373
|
+
? InferOutput<R> | Response | ResponseResult
|
|
439
374
|
: HandlerResult
|
|
440
375
|
|
|
441
376
|
/**
|
|
@@ -497,12 +432,23 @@ const STOP_HOOK_TIMEOUT_MS = 5_000
|
|
|
497
432
|
* for the same request, so a browser that could open a socket was told its POST was cross-origin. */
|
|
498
433
|
const wsSameOrigin = isSameOriginRequest
|
|
499
434
|
|
|
500
|
-
function
|
|
435
|
+
function validationIssues(issues: ReadonlyArray<StandardIssue>): {
|
|
436
|
+
ok: false
|
|
437
|
+
error: string
|
|
438
|
+
issues: unknown[]
|
|
439
|
+
} {
|
|
501
440
|
const serialized = issues.map((issue) => {
|
|
502
441
|
const path = issue.path?.map((seg) => String(typeof seg === "object" ? seg.key : seg))
|
|
503
442
|
return path !== undefined ? { message: issue.message, path } : { message: issue.message }
|
|
504
443
|
})
|
|
505
|
-
return
|
|
444
|
+
return { ok: false, error: "validation", issues: serialized }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** The 422 as plain data - the shape every lane's response wrapper takes, so a rejected body costs
|
|
448
|
+
* what an accepted one costs: on Node it is written straight to the socket with a `content-length`
|
|
449
|
+
* instead of being built as a `Response` and drained back out. */
|
|
450
|
+
export function plainValidationError(issues: ReadonlyArray<StandardIssue>): ResponseResult {
|
|
451
|
+
return status(422, validationIssues(issues))
|
|
506
452
|
}
|
|
507
453
|
|
|
508
454
|
// `jsonError`, `urlPartsOf`, `pathnameOf` moved to `./http.ts` (a dependency-free leaf shared with the
|
|
@@ -557,6 +503,21 @@ function responseSet(ctx: RawContext): CtxSet {
|
|
|
557
503
|
return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS
|
|
558
504
|
}
|
|
559
505
|
|
|
506
|
+
/**
|
|
507
|
+
* A `derive` that answered with a response rather than a context extension - `status(...)`, or a bare
|
|
508
|
+
* `Response`. The request ends there, on the same lane a `beforeHandle`'s early return takes.
|
|
509
|
+
*
|
|
510
|
+
* `derive` is the one stage that could not exit early: `beforeHandle` says "done" by returning a
|
|
511
|
+
* value, but a derive's return value IS the extension, so the only way out was `throw`ing a
|
|
512
|
+
* `Response` - which measured as the single most expensive way to answer (the `Response` object
|
|
513
|
+
* costs ~40% of the rejection lane; see `PlainRender`). Neither shape had a meaning here before:
|
|
514
|
+
* `Object.assign(ctx, response)` copies nothing (a `Response`'s properties live on its prototype),
|
|
515
|
+
* so a derive that returned one silently fell through to the handler it meant to prevent.
|
|
516
|
+
*/
|
|
517
|
+
function isDeriveEarlyExit(extension: unknown): boolean {
|
|
518
|
+
return isResponseResult(extension) || extension instanceof Response
|
|
519
|
+
}
|
|
520
|
+
|
|
560
521
|
function isContextlessNoArgArrow(handler: (context: never) => unknown): boolean {
|
|
561
522
|
if (handler.length !== 0) return false
|
|
562
523
|
try {
|
|
@@ -577,8 +538,17 @@ type FeatureVersionOf<V extends string> = V extends `${infer Major}.${infer Mino
|
|
|
577
538
|
*/
|
|
578
539
|
export type NifraFeatureVersion = FeatureVersionOf<Version>
|
|
579
540
|
|
|
541
|
+
/**
|
|
542
|
+
* The Web lanes' `wrapResponse` input, as a `Response`. A plain-data carrier ({@link plainError},
|
|
543
|
+
* `status(...)`) is rendered on the ordinary JSON lane rather than through its own `toResponse`, so
|
|
544
|
+
* it picks up the same prebuilt init a handler's plain return uses. `EMPTY_RESPONSE_CONTROLS` because
|
|
545
|
+
* these renders happen where no request context exists - before routing, or after it was abandoned.
|
|
546
|
+
*/
|
|
547
|
+
const webResponseOf = (result: Response | ResponseResult): Response =>
|
|
548
|
+
result instanceof Response ? result : toResponse(result, EMPTY_RESPONSE_CONTROLS)
|
|
549
|
+
|
|
580
550
|
// Stable module-level finalizers so `fetch`/`resolveNode` allocate no per-request closures.
|
|
581
|
-
const IDENTITY_RESPONSE = (response: Response): Response => response
|
|
551
|
+
const IDENTITY_RESPONSE = (response: Response | ResponseResult): Response => webResponseOf(response)
|
|
582
552
|
const RESPONSE_TIMEOUT = (): Response => jsonError(503, "request_timeout")
|
|
583
553
|
|
|
584
554
|
// The unused-order-scoped-hook audit (recorder, seal check, stack capture, message) is a dev-time
|
|
@@ -780,7 +750,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
780
750
|
private staticResponseHeaders: StaticResponseHeaders | undefined
|
|
781
751
|
/** `wrapResponse` for the Web lanes: identity until static headers exist to fold into the
|
|
782
752
|
* framework's own error/404/timeout renders, which are built outside the header init. */
|
|
783
|
-
private wrapWebResponse: (response: Response) => Response
|
|
753
|
+
private wrapWebResponse: (response: Response | ResponseResult) => Response
|
|
784
754
|
private webResponseTimeout: () => Response
|
|
785
755
|
/** Body/raw response hooks need a framework-payload marker; keep that decision per app. */
|
|
786
756
|
private responseBodyTag: object | undefined
|
|
@@ -925,8 +895,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
925
895
|
* declared *after* this call, and reaches none declared before. One added after the last route
|
|
926
896
|
* (e.g. on the app returned by a route-registering factory) covers nothing - see
|
|
927
897
|
* {@link ServerOptions.unusedScopedHooks}.
|
|
898
|
+
*
|
|
899
|
+
* Returning a `status(...)` (or a `Response`) instead of an extension ends the request there - the
|
|
900
|
+
* guard shape, and the cheap one: it renders as plain data on the same lane a handler's return
|
|
901
|
+
* takes, where `throw new Response(...)` pays ~40% of the rejection lane to build the object. That
|
|
902
|
+
* branch is control flow, so it does not widen the derived context type.
|
|
928
903
|
*/
|
|
929
|
-
derive<D extends object>(
|
|
904
|
+
derive<D extends object>(
|
|
905
|
+
fn: (context: Context & Ctx) => MaybePromise<D | ResponseResult | Response>,
|
|
906
|
+
): Server<R, Ctx & D> {
|
|
930
907
|
this.assertConfigurable("derive()")
|
|
931
908
|
this.derives.push(fn as unknown as RawDerive)
|
|
932
909
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
@@ -1089,11 +1066,27 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1089
1066
|
* hook owns part of the header state - into a hook that runs in the right order. */
|
|
1090
1067
|
private addStaticResponseHeaders(record: Record<string, string>): this {
|
|
1091
1068
|
if (this.onResponseHooks.length > 0) {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1069
|
+
const observerHeaders = (
|
|
1070
|
+
this as unknown as Pick<ResponseObserverMethods, "onResponseHeaders">
|
|
1071
|
+
).onResponseHeaders
|
|
1072
|
+
if (observerHeaders !== undefined) {
|
|
1073
|
+
observerHeaders.call(this, (headers) => {
|
|
1074
|
+
for (const name of Object.keys(record)) {
|
|
1075
|
+
if (!headers.has(name)) headers.set(name, record[name] as string)
|
|
1076
|
+
}
|
|
1077
|
+
})
|
|
1078
|
+
return this
|
|
1079
|
+
}
|
|
1080
|
+
// Route the hook through the same seam the static tier uses. On a framework response (headers
|
|
1081
|
+
// stamped mutable) it applies the defaults in place and returns the SAME object, so the
|
|
1082
|
+
// serialized-body marker survives for later `onResponseBody`/`onResponseRaw` observers and the
|
|
1083
|
+
// Node writer; reconstructing a `Response` here would strip that marker and reclassify the Node
|
|
1084
|
+
// outcome from `json` to a generic `response`. A guarded foreign response takes its clone path.
|
|
1085
|
+
const statics = buildStaticResponseHeaders(record)
|
|
1086
|
+
this.onResponseHooks.push((response) => applyStaticResponseHeaders(response, statics))
|
|
1087
|
+
this.onNodeResponseHooks.push(undefined)
|
|
1088
|
+
this.nodeResponseHooksComplete = false
|
|
1089
|
+
return this
|
|
1097
1090
|
}
|
|
1098
1091
|
const merged =
|
|
1099
1092
|
this.staticResponseHeaders === undefined
|
|
@@ -1101,7 +1094,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1101
1094
|
: { ...this.staticResponseHeaders.record, ...record }
|
|
1102
1095
|
this.staticResponseHeaders = buildStaticResponseHeaders(merged)
|
|
1103
1096
|
const statics = this.staticResponseHeaders
|
|
1104
|
-
this.wrapWebResponse = (response) =>
|
|
1097
|
+
this.wrapWebResponse = (response) =>
|
|
1098
|
+
applyStaticResponseHeaders(webResponseOf(response), statics)
|
|
1105
1099
|
this.webResponseTimeout = () => applyStaticResponseHeaders(RESPONSE_TIMEOUT(), statics)
|
|
1106
1100
|
return this
|
|
1107
1101
|
}
|
|
@@ -1114,61 +1108,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1114
1108
|
return this.responseBodyTag
|
|
1115
1109
|
}
|
|
1116
1110
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
/**
|
|
1131
|
-
* Register a PORTABLE header-only response hook - one implementation, fast on every runtime.
|
|
1132
|
-
*
|
|
1133
|
-
* The hook receives a mutable case-insensitive header view, a minimal request view, and the
|
|
1134
|
-
* status. On the Web serving paths it runs inside the normal `onResponse` walk against the
|
|
1135
|
-
* response's own `Headers` (mutating in place - no clone). On Node it self-pairs as a native
|
|
1136
|
-
* response hook against the outcome's plain header record, so registering one NEVER forces the
|
|
1137
|
-
* Node adapter off its direct socket writer the way a full `onResponse(res: Response)` hook does.
|
|
1138
|
-
* Prefer this over `onResponse` whenever the middleware only reads/writes headers.
|
|
1139
|
-
*/
|
|
1140
|
-
onResponseHeaders(fn: ResponseHeadersHook): this {
|
|
1141
|
-
this.assertConfigurable("onResponseHeaders()")
|
|
1142
|
-
this.onResponseHooks.push(webResponseHeadersHook(fn))
|
|
1143
|
-
this.onNodeResponseHooks.push((response, req) =>
|
|
1144
|
-
fn(recordHeadersView(response), req, response.status),
|
|
1145
|
-
)
|
|
1146
|
-
return this
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
/**
|
|
1150
|
-
* Register a PORTABLE post-serialization body hook - the payload tier. The hook receives the
|
|
1151
|
-
* FINAL framework-serialized bytes (plus the mutable header view and status) and may return
|
|
1152
|
-
* replacement bytes; `undefined` keeps the body. On the Node direct writer the bytes come
|
|
1153
|
-
* straight off the outcome record; on the Web serving paths they ride the framework-built
|
|
1154
|
-
* Response as a tag, so no body stream is ever drained on any runtime. A handler-returned raw
|
|
1155
|
-
* `Response` (proxied fetch, SSE, streamed SSR) is skipped by contract - transforming those is
|
|
1156
|
-
* what the full `onResponse` hook is for.
|
|
1157
|
-
*/
|
|
1158
|
-
onResponseBody(fn: ResponseBodyHook): this {
|
|
1159
|
-
this.assertConfigurable("onResponseBody()")
|
|
1160
|
-
this.enableResponseBodyTagging()
|
|
1161
|
-
this.onResponseHooks.push(webResponseBodyHook(fn, this.responseBodyOwners))
|
|
1162
|
-
this.onNodeResponseHooks.push((response, req) => {
|
|
1163
|
-
const body = response.body
|
|
1164
|
-
if (body === null) return undefined
|
|
1165
|
-
const out = fn(body, recordHeadersView(response), req, response.status)
|
|
1166
|
-
if (out instanceof Promise)
|
|
1167
|
-
return out.then((replaced) => applyBodyReplacement(response, replaced))
|
|
1168
|
-
applyBodyReplacement(response, out)
|
|
1169
|
-
return undefined
|
|
1170
|
-
})
|
|
1171
|
-
return this
|
|
1111
|
+
private responseObserverMethods(): ResponseObserverMethods {
|
|
1112
|
+
const methods = this as unknown as Partial<ResponseObserverMethods>
|
|
1113
|
+
if (
|
|
1114
|
+
typeof methods.onResponseHeaders !== "function" ||
|
|
1115
|
+
typeof methods.onResponseBody !== "function" ||
|
|
1116
|
+
typeof methods.onResponseRaw !== "function"
|
|
1117
|
+
) {
|
|
1118
|
+
throw new TypeError(
|
|
1119
|
+
"response observation requires `.use(responseObserver())` or an observer-enabled middleware",
|
|
1120
|
+
)
|
|
1121
|
+
}
|
|
1122
|
+
return methods as ResponseObserverMethods
|
|
1172
1123
|
}
|
|
1173
1124
|
|
|
1174
1125
|
/** Observe the terminal response after all transformations. Observers are ordered and fail-open. */
|
|
@@ -1180,10 +1131,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1180
1131
|
return this
|
|
1181
1132
|
}
|
|
1182
1133
|
|
|
1134
|
+
/** Enable the opt-in portable response observer methods. */
|
|
1135
|
+
use(plugin: ResponseObserverPlugin): this & ResponseObserverMethods
|
|
1183
1136
|
/**
|
|
1184
1137
|
* Apply a type-**identity** plugin ({@link IdentityPlugin}, from {@link defineIdentityPlugin}) - it
|
|
1185
1138
|
* registers routes/hooks but doesn't change the types, so this returns `this` with the route registry
|
|
1186
|
-
* and context fully intact. This overload exists specifically so a
|
|
1139
|
+
* and context fully intact. This overload exists specifically so a named identity plugin (e.g.
|
|
1187
1140
|
* `@nifrajs/better-auth`) threads the registry: its `& { pluginName }` intersection would otherwise
|
|
1188
1141
|
* defeat the generic inference of the transforming overload below and collapse the result to `any`.
|
|
1189
1142
|
*/
|
|
@@ -1254,6 +1207,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1254
1207
|
}
|
|
1255
1208
|
this.globalAssurance.push(...evidence.filter((item) => item.scope === "global"))
|
|
1256
1209
|
this.activeAssurance.push(...evidence.filter((item) => item.scope === "subsequent"))
|
|
1210
|
+
const responseObserver = (arg as unknown as Record<symbol, unknown>)[INSTALL_RESPONSE_OBSERVER]
|
|
1211
|
+
if (responseObserver !== undefined) {
|
|
1212
|
+
if (
|
|
1213
|
+
typeof responseObserver !== "object" ||
|
|
1214
|
+
responseObserver === null ||
|
|
1215
|
+
typeof (responseObserver as { install?: unknown }).install !== "function"
|
|
1216
|
+
) {
|
|
1217
|
+
throw new TypeError("response observer middleware has an invalid runtime")
|
|
1218
|
+
}
|
|
1219
|
+
this[INSTALL_RESPONSE_OBSERVER](responseObserver as ResponseObserverRuntime)
|
|
1220
|
+
}
|
|
1257
1221
|
if (arg.onRequest !== undefined) {
|
|
1258
1222
|
this.assertConfigurable("onRequest()")
|
|
1259
1223
|
this.onRequestHooks.push(arg.onRequest as RawOnRequest)
|
|
@@ -1277,9 +1241,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1277
1241
|
// Before the bundle's own hooks: a bundle declaring both means its static values are the
|
|
1278
1242
|
// defaults its hook may then override, which is the order a single bundle reads in.
|
|
1279
1243
|
if (arg.responseHeaders !== undefined) this.responseHeaders(arg.responseHeaders)
|
|
1280
|
-
if (arg.onResponseHeaders !== undefined)
|
|
1281
|
-
|
|
1282
|
-
if (arg.
|
|
1244
|
+
if (arg.onResponseHeaders !== undefined)
|
|
1245
|
+
this.responseObserverMethods().onResponseHeaders(arg.onResponseHeaders)
|
|
1246
|
+
if (arg.onResponseBody !== undefined)
|
|
1247
|
+
this.responseObserverMethods().onResponseBody(arg.onResponseBody)
|
|
1248
|
+
if (arg.onResponseRaw !== undefined)
|
|
1249
|
+
this.responseObserverMethods().onResponseRaw(arg.onResponseRaw)
|
|
1283
1250
|
if (arg.onResponseFinalized !== undefined) this.onResponseFinalized(arg.onResponseFinalized)
|
|
1284
1251
|
if (arg.onError !== undefined) this.onError(arg.onError)
|
|
1285
1252
|
return this
|
|
@@ -1547,6 +1514,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1547
1514
|
// Boot-time guard: the WS runtime is a subpath (`@nifrajs/core/ws`) so no-WebSocket apps don't
|
|
1548
1515
|
// bundle it. Registration is the loud, early failure point - never the first connection.
|
|
1549
1516
|
const runtime = requireWsRuntime(this.wsRuntime)
|
|
1517
|
+
if (handler.validateSend === true && handler.sendSchema === undefined) {
|
|
1518
|
+
throw new RouteConfigError(
|
|
1519
|
+
"INVALID_WS_SEND_VALIDATION",
|
|
1520
|
+
`route WS ${path}: validateSend requires sendSchema`,
|
|
1521
|
+
)
|
|
1522
|
+
}
|
|
1550
1523
|
this.topics ??= runtime.createTopics()
|
|
1551
1524
|
// A `messageSchema` wraps `message` with validation once, here - every adapter then dispatches
|
|
1552
1525
|
// already-validated, typed messages (Bun/Deno/Node/Workers) with no per-adapter code.
|
|
@@ -1627,6 +1600,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1627
1600
|
handler: (context: never) => unknown,
|
|
1628
1601
|
): CatalogRoute {
|
|
1629
1602
|
const pattern = compileRoutePattern(path)
|
|
1603
|
+
// Resolved here, once: `undefined` past this point means the route declared `"unlimited"`, NOT
|
|
1604
|
+
// "nothing declared". Readers fall back to UNLIMITED_BODY_BYTES accordingly.
|
|
1630
1605
|
let bodyLimit: number | undefined = this.maxBodyBytes
|
|
1631
1606
|
const invalidBodyLimit = (message: string): never => {
|
|
1632
1607
|
throw new RouteConfigError("INVALID_BODY_LIMIT", `route ${method} ${path}: ${message}`)
|
|
@@ -1641,6 +1616,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1641
1616
|
) {
|
|
1642
1617
|
invalidBodyLimit('bodyLimit: "unlimited" requires a non-empty bodyLimitReason')
|
|
1643
1618
|
}
|
|
1619
|
+
if (schema.body !== undefined) {
|
|
1620
|
+
invalidBodyLimit(
|
|
1621
|
+
'bodyLimit: "unlimited" cannot be used with a body schema; use a finite cap',
|
|
1622
|
+
)
|
|
1623
|
+
}
|
|
1624
|
+
if (schema.idempotency !== undefined) {
|
|
1625
|
+
invalidBodyLimit('bodyLimit: "unlimited" cannot be used with idempotency')
|
|
1626
|
+
}
|
|
1644
1627
|
bodyLimit = undefined
|
|
1645
1628
|
} else if (schema?.bodyLimit !== undefined) {
|
|
1646
1629
|
try {
|
|
@@ -1769,7 +1752,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1769
1752
|
handler as unknown as InternalHandler,
|
|
1770
1753
|
schema.body as StandardSchemaV1,
|
|
1771
1754
|
hasDecorations ? routeDecorations : undefined,
|
|
1772
|
-
bodyLimit ??
|
|
1755
|
+
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
1773
1756
|
)
|
|
1774
1757
|
: undefined
|
|
1775
1758
|
const fusedWeb =
|
|
@@ -1778,14 +1761,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1778
1761
|
handler as unknown as InternalHandler,
|
|
1779
1762
|
hasDecorations ? routeDecorations : undefined,
|
|
1780
1763
|
isContextlessNoArgArrow(handler),
|
|
1781
|
-
bodyLimit ??
|
|
1764
|
+
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
1782
1765
|
)
|
|
1783
1766
|
: fusedQuery
|
|
1784
1767
|
? this.buildFusedQueryWeb(
|
|
1785
1768
|
handler as unknown as InternalHandler,
|
|
1786
1769
|
hasDecorations ? routeDecorations : undefined,
|
|
1787
1770
|
schema.query as StandardSchemaV1,
|
|
1788
|
-
bodyLimit ??
|
|
1771
|
+
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
1789
1772
|
)
|
|
1790
1773
|
: fusedBody
|
|
1791
1774
|
? this.buildFusedBodyWeb(fusedBodyRunner as FusedBodyRunner)
|
|
@@ -1901,7 +1884,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1901
1884
|
})
|
|
1902
1885
|
routeAssurance.push(Object.freeze(declared))
|
|
1903
1886
|
}
|
|
1904
|
-
|
|
1887
|
+
// Two ways a route proves its body is bounded, both enforced by the core at read time:
|
|
1888
|
+
// `schema.body` (the validated read is bounded) and an explicit finite `schema.bodyLimit` (the
|
|
1889
|
+
// transport cap shadows every reader, including a raw `c.req.body` stream that a Content-Length
|
|
1890
|
+
// middleware cannot bound).
|
|
1891
|
+
// `bodyLimit: "unlimited"` is rejected above for schema and idempotency routes. The remaining
|
|
1892
|
+
// exemption is schema-less streaming/upload work, so it publishes NOTHING: there is no finite
|
|
1893
|
+
// framework byte ceiling for assurance to attest and the application protocol owns that bound.
|
|
1894
|
+
// The server-wide `maxBodyBytes` default is not evidence either. It applies to every route
|
|
1895
|
+
// whether or not anyone thought about it, so publishing it would make the id pass everywhere and
|
|
1896
|
+
// prove nothing; this evidence marks a bound the route chose.
|
|
1897
|
+
const unlimitedBody = schema?.bodyLimit === "unlimited"
|
|
1898
|
+
if (!unlimitedBody && (schema?.body !== undefined || typeof schema?.bodyLimit === "number")) {
|
|
1905
1899
|
routeAssurance.push(
|
|
1906
1900
|
Object.freeze({
|
|
1907
1901
|
id: NIFRA_ASSURANCE_IDS.BODY_BOUNDED,
|
|
@@ -1961,6 +1955,29 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
1961
1955
|
this.responseContractRuntime = runtime
|
|
1962
1956
|
}
|
|
1963
1957
|
|
|
1958
|
+
/** @internal Symbol-keyed install seam for the response observer plugin. */
|
|
1959
|
+
[INSTALL_RESPONSE_OBSERVER](runtime: ResponseObserverRuntime): ResponseObserverMethods {
|
|
1960
|
+
this.assertConfigurable("responseObserver()")
|
|
1961
|
+
const methods = runtime.install({
|
|
1962
|
+
assertConfigurable: (operation) => this.assertConfigurable(operation),
|
|
1963
|
+
addResponseHook: (web, node) => {
|
|
1964
|
+
this.onResponseHooks.push(web)
|
|
1965
|
+
this.onNodeResponseHooks.push(node)
|
|
1966
|
+
if (node === undefined) this.nodeResponseHooksComplete = false
|
|
1967
|
+
},
|
|
1968
|
+
enableResponseBodyTagging: () => {
|
|
1969
|
+
if (this.responseBodyTag === undefined) {
|
|
1970
|
+
this.responseBodyTag = Object.freeze({})
|
|
1971
|
+
this.responseBodyOwners.add(this.responseBodyTag)
|
|
1972
|
+
}
|
|
1973
|
+
return this.responseBodyTag
|
|
1974
|
+
},
|
|
1975
|
+
responseBodyOwners: () => this.responseBodyOwners,
|
|
1976
|
+
})
|
|
1977
|
+
Object.assign(this, methods)
|
|
1978
|
+
return methods
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1964
1981
|
/** @internal Symbol-keyed install seam for the `idempotency()` plugin. Off the public typed surface. */
|
|
1965
1982
|
[INSTALL_IDEMPOTENCY](runtime: IdempotencyRuntime): void {
|
|
1966
1983
|
this.assertConfigurable("idempotency()")
|
|
@@ -2116,7 +2133,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2116
2133
|
entry.handler,
|
|
2117
2134
|
entry.schema?.body as StandardSchemaV1,
|
|
2118
2135
|
entry.hasDecorations ? entry.decorations : undefined,
|
|
2119
|
-
entry.bodyLimit ??
|
|
2136
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2120
2137
|
)
|
|
2121
2138
|
: undefined
|
|
2122
2139
|
const fusedWeb =
|
|
@@ -2127,13 +2144,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2127
2144
|
entry.handler,
|
|
2128
2145
|
entry.hasDecorations ? entry.decorations : undefined,
|
|
2129
2146
|
entry.schema?.query as StandardSchemaV1,
|
|
2130
|
-
entry.bodyLimit ??
|
|
2147
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2131
2148
|
)
|
|
2132
2149
|
: this.buildFusedWeb(
|
|
2133
2150
|
entry.handler,
|
|
2134
2151
|
entry.hasDecorations ? entry.decorations : undefined,
|
|
2135
2152
|
isContextlessNoArgArrow(entry.handler),
|
|
2136
|
-
entry.bodyLimit ??
|
|
2153
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2137
2154
|
)
|
|
2138
2155
|
return {
|
|
2139
2156
|
...route,
|
|
@@ -2290,6 +2307,45 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2290
2307
|
return outcome
|
|
2291
2308
|
}
|
|
2292
2309
|
|
|
2310
|
+
/**
|
|
2311
|
+
* Select a mounted handler's native Node lane without materializing a Web Request. This is an
|
|
2312
|
+
* adapter seam, not a second lifecycle: any app-wide behavior that the ordinary mount path would
|
|
2313
|
+
* run makes the capability ineligible and the caller falls back to `resolveNodeSource`. Typed
|
|
2314
|
+
* routes keep precedence exactly as `routeAndRun` does, including the existing mount behavior for
|
|
2315
|
+
* a path whose registered route rejects this method.
|
|
2316
|
+
*/
|
|
2317
|
+
[RESOLVE_NODE_MOUNT](source: RequestSource): NativeMountSelection | undefined {
|
|
2318
|
+
if (
|
|
2319
|
+
this.onRequestHooks.length > 0 ||
|
|
2320
|
+
this.onResponseHooks.length > 0 ||
|
|
2321
|
+
this.onResponseFinalizedHooks.length > 0 ||
|
|
2322
|
+
this.capacityGate !== undefined ||
|
|
2323
|
+
this.staticResponseHeaders !== undefined ||
|
|
2324
|
+
this.requestTimeoutMs !== 0 ||
|
|
2325
|
+
this.acceptInboundDeadlines ||
|
|
2326
|
+
this.clientIpTrust !== undefined
|
|
2327
|
+
) {
|
|
2328
|
+
return undefined
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
const parts = source.urlParts ?? urlPartsOf(source.url)
|
|
2332
|
+
const match = this.catalog.find(source.method, parts.pathname)
|
|
2333
|
+
if (match.found) return undefined
|
|
2334
|
+
|
|
2335
|
+
for (const mount of this.fetchMounts) {
|
|
2336
|
+
if (!underMountPrefix(parts.pathname, mount.path)) continue
|
|
2337
|
+
const candidate = (mount.handler as unknown as Record<symbol, unknown>)[NODE_NATIVE_MOUNT]
|
|
2338
|
+
return typeof candidate === "function"
|
|
2339
|
+
? {
|
|
2340
|
+
handler: candidate as NativeMountHandler,
|
|
2341
|
+
path: mount.path,
|
|
2342
|
+
stripPrefix: mount.stripPrefix,
|
|
2343
|
+
}
|
|
2344
|
+
: undefined
|
|
2345
|
+
}
|
|
2346
|
+
return undefined
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2293
2349
|
/**
|
|
2294
2350
|
* Like {@link fetch}, but renders a plain-data result **without** building a Web `Response` - the
|
|
2295
2351
|
* `@nifrajs/node` adapter serializes the returned primitives straight to the socket, skipping the undici
|
|
@@ -2668,7 +2724,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2668
2724
|
source: RequestSource,
|
|
2669
2725
|
platform: Platform | undefined,
|
|
2670
2726
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2671
|
-
wrapResponse: (response: Response) => T,
|
|
2727
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2672
2728
|
onTimeout: () => T,
|
|
2673
2729
|
// True only from the Web `fetch` path - unlocks each route's fused lane, whose output type IS
|
|
2674
2730
|
// `Response` (`T = Response` there by construction; the node path always passes false).
|
|
@@ -2696,7 +2752,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2696
2752
|
source: RequestSource,
|
|
2697
2753
|
platform: Platform | undefined,
|
|
2698
2754
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2699
|
-
wrapResponse: (response: Response) => T,
|
|
2755
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2700
2756
|
onTimeout: () => T,
|
|
2701
2757
|
): MaybePromise<T> {
|
|
2702
2758
|
const hooks = this.onNodeRequestHooks
|
|
@@ -2730,7 +2786,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2730
2786
|
source: RequestSource,
|
|
2731
2787
|
platform: Platform | undefined,
|
|
2732
2788
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2733
|
-
wrapResponse: (response: Response) => T,
|
|
2789
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2734
2790
|
onTimeout: () => T,
|
|
2735
2791
|
): Promise<T> {
|
|
2736
2792
|
if (first !== undefined) return wrapResponse(first)
|
|
@@ -2758,7 +2814,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2758
2814
|
source: RequestSource,
|
|
2759
2815
|
platform: Platform | undefined,
|
|
2760
2816
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2761
|
-
wrapResponse: (response: Response) => T,
|
|
2817
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2762
2818
|
onTimeout: () => T,
|
|
2763
2819
|
webFast: boolean,
|
|
2764
2820
|
): MaybePromise<T> {
|
|
@@ -2805,7 +2861,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2805
2861
|
sourceAtAwait: RequestSource,
|
|
2806
2862
|
platform: Platform | undefined,
|
|
2807
2863
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2808
|
-
wrapResponse: (response: Response) => T,
|
|
2864
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2809
2865
|
onTimeout: () => T,
|
|
2810
2866
|
webFast: boolean,
|
|
2811
2867
|
): Promise<T> {
|
|
@@ -2899,7 +2955,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2899
2955
|
source: RequestSource,
|
|
2900
2956
|
platform: Platform | undefined,
|
|
2901
2957
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
2902
|
-
wrapResponse: (response: Response) => T,
|
|
2958
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
2903
2959
|
onTimeout: () => T,
|
|
2904
2960
|
webFast: boolean,
|
|
2905
2961
|
): MaybePromise<T> {
|
|
@@ -2951,17 +3007,19 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2951
3007
|
}
|
|
2952
3008
|
if (match.reason === "method-not-allowed") {
|
|
2953
3009
|
return wrapResponse(
|
|
2954
|
-
|
|
3010
|
+
// Lowercase on purpose: a plain render's headers go into the node outcome record verbatim,
|
|
3011
|
+
// and every other name in that record is lowercase (the Web lane normalizes either way).
|
|
3012
|
+
plainError(405, "method_not_allowed", { allow: match.allowed.join(", ") }),
|
|
2955
3013
|
)
|
|
2956
3014
|
}
|
|
2957
|
-
return wrapResponse(
|
|
3015
|
+
return wrapResponse(plainError(404, "not_found"))
|
|
2958
3016
|
}
|
|
2959
3017
|
|
|
2960
3018
|
// Inspect only captured values for escapes. Scanning the full pathname repeated work the router
|
|
2961
3019
|
// already did and made every plain dynamic route pay for unrelated static path bytes.
|
|
2962
3020
|
const params = match.params === EMPTY_PARAMS ? match.params : decodeRouteParams(match.params)
|
|
2963
3021
|
if (params === null) {
|
|
2964
|
-
return wrapResponse(
|
|
3022
|
+
return wrapResponse(plainError(400, "malformed_path"))
|
|
2965
3023
|
}
|
|
2966
3024
|
|
|
2967
3025
|
return this.runMatched(
|
|
@@ -3001,7 +3059,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3001
3059
|
params: Record<string, string>,
|
|
3002
3060
|
search: string | undefined,
|
|
3003
3061
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3004
|
-
wrapResponse: (response: Response) => T,
|
|
3062
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3005
3063
|
onTimeout: () => T,
|
|
3006
3064
|
webFast: boolean,
|
|
3007
3065
|
): MaybePromise<T> {
|
|
@@ -3032,7 +3090,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3032
3090
|
search,
|
|
3033
3091
|
wrapResponse,
|
|
3034
3092
|
{
|
|
3035
|
-
maxBodyBytes: entry.bodyLimit ??
|
|
3093
|
+
maxBodyBytes: entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3036
3094
|
runLanes: (buffered, plat, ent, prm, srch) =>
|
|
3037
3095
|
this.idempotencyRunLanes(buffered, plat, ent as RouteEntry, prm, srch),
|
|
3038
3096
|
},
|
|
@@ -3059,7 +3117,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3059
3117
|
params: Record<string, string>,
|
|
3060
3118
|
search: string | undefined,
|
|
3061
3119
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3062
|
-
wrapResponse: (response: Response) => T,
|
|
3120
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3063
3121
|
onTimeout: () => T,
|
|
3064
3122
|
webFast: boolean,
|
|
3065
3123
|
): MaybePromise<T> {
|
|
@@ -3081,7 +3139,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3081
3139
|
? undefined
|
|
3082
3140
|
: admitDeadline(source.headers, this.deadlineAdmissionOptions)
|
|
3083
3141
|
if (admission !== undefined && !admission.ok) {
|
|
3084
|
-
return wrapResponse(
|
|
3142
|
+
return wrapResponse(plainError(admission.status, admission.reason))
|
|
3085
3143
|
}
|
|
3086
3144
|
const effectiveTimeoutMs = admission?.timeoutMs ?? 0
|
|
3087
3145
|
|
|
@@ -3140,7 +3198,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3140
3198
|
if (controller !== undefined && outcome instanceof Promise) {
|
|
3141
3199
|
const timedOut =
|
|
3142
3200
|
admission?.inherited === true
|
|
3143
|
-
? () => wrapResponse(
|
|
3201
|
+
? () => wrapResponse(plainError(504, "deadline_exceeded"))
|
|
3144
3202
|
: onTimeout
|
|
3145
3203
|
return this.withTimeout(
|
|
3146
3204
|
outcome,
|
|
@@ -3171,7 +3229,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3171
3229
|
budget: RequestBudget,
|
|
3172
3230
|
platform: Platform | undefined,
|
|
3173
3231
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3174
|
-
wrapResponse: (response: Response) => T,
|
|
3232
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3175
3233
|
): MaybePromise<T> {
|
|
3176
3234
|
let result: unknown
|
|
3177
3235
|
try {
|
|
@@ -3215,9 +3273,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3215
3273
|
signal: AbortSignal,
|
|
3216
3274
|
budget: RequestBudget,
|
|
3217
3275
|
platform: Platform | undefined,
|
|
3218
|
-
wrapResponse: (response: Response) => T,
|
|
3276
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3219
3277
|
): T {
|
|
3220
3278
|
if (err instanceof Response) return wrapResponse(err)
|
|
3279
|
+
// Handed over as the plain data it is: `wrapResponse` renders it on the JSON lane (no `Response`
|
|
3280
|
+
// built on Node). There is no context here by construction, so there is no `c.set` to merge.
|
|
3281
|
+
if (isResponseResult(err)) return wrapResponse(err)
|
|
3221
3282
|
const ctx = new RequestContext(
|
|
3222
3283
|
source,
|
|
3223
3284
|
params,
|
|
@@ -3229,7 +3290,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3229
3290
|
this.protoPoisoning,
|
|
3230
3291
|
)
|
|
3231
3292
|
this.logRequestError(err, ctx)
|
|
3232
|
-
return wrapResponse(
|
|
3293
|
+
return wrapResponse(plainError(500, "internal_error"))
|
|
3233
3294
|
}
|
|
3234
3295
|
|
|
3235
3296
|
/**
|
|
@@ -3246,20 +3307,20 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3246
3307
|
entry: RouteEntry,
|
|
3247
3308
|
ctx: RawContext,
|
|
3248
3309
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3249
|
-
wrapResponse: (response: Response) => T,
|
|
3310
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3250
3311
|
): MaybePromise<T> {
|
|
3251
3312
|
let result: unknown
|
|
3252
3313
|
try {
|
|
3253
3314
|
if (entry.hasDecorations) Object.assign(ctx, entry.decorations)
|
|
3254
3315
|
result = entry.handler(ctx)
|
|
3255
3316
|
} catch (err) {
|
|
3256
|
-
return this.bareError(err, ctx, wrapResponse)
|
|
3317
|
+
return this.bareError(err, ctx, finalize, wrapResponse)
|
|
3257
3318
|
}
|
|
3258
3319
|
if (result instanceof Promise) {
|
|
3259
3320
|
// Async handler on an otherwise-bare route: finish on a microtask, with the same error handling.
|
|
3260
3321
|
return result.then(
|
|
3261
3322
|
(value) => finalize(value, responseSet(ctx)),
|
|
3262
|
-
(err) => this.bareError(err, ctx, wrapResponse),
|
|
3323
|
+
(err) => this.bareError(err, ctx, finalize, wrapResponse),
|
|
3263
3324
|
)
|
|
3264
3325
|
}
|
|
3265
3326
|
return finalize(result, responseSet(ctx))
|
|
@@ -3280,13 +3341,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3280
3341
|
contextless: boolean,
|
|
3281
3342
|
maxBodyBytes: number,
|
|
3282
3343
|
): FusedWebRunner {
|
|
3283
|
-
//
|
|
3284
|
-
//
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
this.
|
|
3288
|
-
|
|
3289
|
-
|
|
3344
|
+
// Same thrown-value contract as the generic lane (`bare-error-lane.ts` via `bareError`); the fused
|
|
3345
|
+
// lane only injects its renderer, so a thrown `status(...)` and the flat 500 fold in static headers
|
|
3346
|
+
// through `wrapWebResponse`. One source of truth for the error surface, no per-lane copy to drift.
|
|
3347
|
+
const logError = (err: unknown, ctx: RawContext): Response =>
|
|
3348
|
+
this.bareError(
|
|
3349
|
+
err,
|
|
3350
|
+
ctx,
|
|
3351
|
+
(result, set) => this.wrapWebResponse(toResponse(result, set)),
|
|
3352
|
+
this.wrapWebResponse,
|
|
3353
|
+
)
|
|
3290
3354
|
if (contextless && decorations === undefined) {
|
|
3291
3355
|
// `() => ...` can't observe the context - skip allocating one entirely (errors still build
|
|
3292
3356
|
// one for the structured log, exactly like runContextlessBare).
|
|
@@ -3416,20 +3480,21 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3416
3480
|
const logError = <T>(
|
|
3417
3481
|
err: unknown,
|
|
3418
3482
|
ctx: RawContext,
|
|
3419
|
-
|
|
3420
|
-
|
|
3483
|
+
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
3484
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3485
|
+
): T => this.bareError(err, ctx, finalize, wrapResponse)
|
|
3421
3486
|
|
|
3422
3487
|
const runHandler = <T>(
|
|
3423
3488
|
ctx: RawContext,
|
|
3424
3489
|
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
3425
|
-
wrapResponse: (response: Response) => T,
|
|
3490
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3426
3491
|
): MaybePromise<T> => {
|
|
3427
3492
|
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
3428
3493
|
let output: MaybePromise<HandlerResult>
|
|
3429
3494
|
try {
|
|
3430
3495
|
output = handler(ctx)
|
|
3431
3496
|
} catch (err) {
|
|
3432
|
-
return logError(err, ctx, wrapResponse)
|
|
3497
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3433
3498
|
}
|
|
3434
3499
|
if (output instanceof Promise) {
|
|
3435
3500
|
return output.then(
|
|
@@ -3437,16 +3502,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3437
3502
|
try {
|
|
3438
3503
|
return finalize(result, responseSet(ctx), ctx)
|
|
3439
3504
|
} catch (err) {
|
|
3440
|
-
return logError(err, ctx, wrapResponse)
|
|
3505
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3441
3506
|
}
|
|
3442
3507
|
},
|
|
3443
|
-
(err) => logError(err, ctx, wrapResponse),
|
|
3508
|
+
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
3444
3509
|
)
|
|
3445
3510
|
}
|
|
3446
3511
|
try {
|
|
3447
3512
|
return finalize(output, responseSet(ctx), ctx)
|
|
3448
3513
|
} catch (err) {
|
|
3449
|
-
return logError(err, ctx, wrapResponse)
|
|
3514
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3450
3515
|
}
|
|
3451
3516
|
}
|
|
3452
3517
|
|
|
@@ -3454,9 +3519,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3454
3519
|
result: StandardResult<unknown>,
|
|
3455
3520
|
ctx: RawContext,
|
|
3456
3521
|
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
3457
|
-
wrapResponse: (response: Response) => T,
|
|
3522
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3458
3523
|
): MaybePromise<T> => {
|
|
3459
|
-
if (result.issues !== undefined) return wrapResponse(
|
|
3524
|
+
if (result.issues !== undefined) return wrapResponse(plainValidationError(result.issues))
|
|
3460
3525
|
ctx.body = result.value
|
|
3461
3526
|
return runHandler(ctx, finalize, wrapResponse)
|
|
3462
3527
|
}
|
|
@@ -3465,13 +3530,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3465
3530
|
parsed: unknown,
|
|
3466
3531
|
ctx: RawContext,
|
|
3467
3532
|
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
3468
|
-
wrapResponse: (response: Response) => T,
|
|
3533
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3469
3534
|
): MaybePromise<T> => {
|
|
3470
3535
|
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
3471
3536
|
try {
|
|
3472
3537
|
validation = bodySchema["~standard"].validate(parsed)
|
|
3473
3538
|
} catch (err) {
|
|
3474
|
-
return logError(err, ctx, wrapResponse)
|
|
3539
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3475
3540
|
}
|
|
3476
3541
|
if (validation instanceof Promise) {
|
|
3477
3542
|
return validation.then(
|
|
@@ -3479,16 +3544,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3479
3544
|
try {
|
|
3480
3545
|
return runValidated(settled, ctx, finalize, wrapResponse)
|
|
3481
3546
|
} catch (err) {
|
|
3482
|
-
return logError(err, ctx, wrapResponse)
|
|
3547
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3483
3548
|
}
|
|
3484
3549
|
},
|
|
3485
|
-
(err) => logError(err, ctx, wrapResponse),
|
|
3550
|
+
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
3486
3551
|
)
|
|
3487
3552
|
}
|
|
3488
3553
|
try {
|
|
3489
3554
|
return runValidated(validation, ctx, finalize, wrapResponse)
|
|
3490
3555
|
} catch (err) {
|
|
3491
|
-
return logError(err, ctx, wrapResponse)
|
|
3556
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
3492
3557
|
}
|
|
3493
3558
|
}
|
|
3494
3559
|
|
|
@@ -3501,7 +3566,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3501
3566
|
platform: Platform | undefined,
|
|
3502
3567
|
nativeContext: boolean,
|
|
3503
3568
|
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
3504
|
-
wrapResponse: (response: Response) => T,
|
|
3569
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3505
3570
|
): MaybePromise<T> => {
|
|
3506
3571
|
const ctx = nativeContext
|
|
3507
3572
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
@@ -3517,8 +3582,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3517
3582
|
)
|
|
3518
3583
|
const finish = (value: unknown): MaybePromise<T> =>
|
|
3519
3584
|
runParsed(value, ctx, finalize, wrapResponse)
|
|
3520
|
-
return
|
|
3521
|
-
|
|
3585
|
+
return readBodyFramed(
|
|
3586
|
+
source,
|
|
3587
|
+
maxBodyBytes,
|
|
3588
|
+
this.protoPoisoning,
|
|
3589
|
+
finish,
|
|
3590
|
+
wrapResponse,
|
|
3591
|
+
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
3522
3592
|
)
|
|
3523
3593
|
}
|
|
3524
3594
|
}
|
|
@@ -3560,13 +3630,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3560
3630
|
querySchema: StandardSchemaV1,
|
|
3561
3631
|
maxBodyBytes: number,
|
|
3562
3632
|
): FusedWebRunner {
|
|
3563
|
-
//
|
|
3564
|
-
//
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
this.
|
|
3568
|
-
|
|
3569
|
-
|
|
3633
|
+
// Same thrown-value contract as the generic lane (`bare-error-lane.ts` via `bareError`); the fused
|
|
3634
|
+
// lane only injects its renderer, so a thrown `status(...)` and the flat 500 fold in static headers
|
|
3635
|
+
// through `wrapWebResponse`. One source of truth for the error surface, no per-lane copy to drift.
|
|
3636
|
+
const logError = (err: unknown, ctx: RawContext): Response =>
|
|
3637
|
+
this.bareError(
|
|
3638
|
+
err,
|
|
3639
|
+
ctx,
|
|
3640
|
+
(result, set) => this.wrapWebResponse(toResponse(result, set)),
|
|
3641
|
+
this.wrapWebResponse,
|
|
3642
|
+
)
|
|
3570
3643
|
const runHandler = (ctx: RawContext, value: unknown): MaybePromise<Response> => {
|
|
3571
3644
|
ctx.query = value
|
|
3572
3645
|
let result: unknown
|
|
@@ -3620,21 +3693,29 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3620
3693
|
return validation.then(
|
|
3621
3694
|
(settled) =>
|
|
3622
3695
|
settled.issues !== undefined
|
|
3623
|
-
? this.wrapWebResponse(
|
|
3696
|
+
? this.wrapWebResponse(plainValidationError(settled.issues))
|
|
3624
3697
|
: runHandler(ctx, settled.value),
|
|
3625
3698
|
(err) => logError(err, ctx),
|
|
3626
3699
|
)
|
|
3627
3700
|
}
|
|
3628
3701
|
if (validation.issues !== undefined)
|
|
3629
|
-
return this.wrapWebResponse(
|
|
3702
|
+
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
3630
3703
|
return runHandler(ctx, validation.value)
|
|
3631
3704
|
}
|
|
3632
3705
|
}
|
|
3633
3706
|
|
|
3634
|
-
private bareError<T>(
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3707
|
+
private bareError<T>(
|
|
3708
|
+
err: unknown,
|
|
3709
|
+
ctx: RawContext,
|
|
3710
|
+
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
3711
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3712
|
+
): T {
|
|
3713
|
+
// The thrown-value contract lives in `bare-error-lane.ts`, one source of truth for both this
|
|
3714
|
+
// lane and the fused body runner. `responseSet` and the logger are injected because they reach
|
|
3715
|
+
// into class state; the closure is allocated only on the throw path, never on the hot lane.
|
|
3716
|
+
return renderBareError(err, ctx, finalize, wrapResponse, responseSet, (e, c) =>
|
|
3717
|
+
this.logRequestError(e, c),
|
|
3718
|
+
)
|
|
3638
3719
|
}
|
|
3639
3720
|
|
|
3640
3721
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
@@ -3644,7 +3725,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3644
3725
|
ctx: RawContext,
|
|
3645
3726
|
run: () => MaybePromise<T>,
|
|
3646
3727
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3647
|
-
wrapResponse: (response: Response) => T,
|
|
3728
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3648
3729
|
): MaybePromise<T> {
|
|
3649
3730
|
let outcome: MaybePromise<T>
|
|
3650
3731
|
try {
|
|
@@ -3682,52 +3763,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3682
3763
|
source: RequestSource,
|
|
3683
3764
|
ctx: RawContext,
|
|
3684
3765
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3685
|
-
wrapResponse: (response: Response) => T,
|
|
3766
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3686
3767
|
): Promise<T> {
|
|
3687
|
-
return
|
|
3768
|
+
return readBodyFramed(
|
|
3688
3769
|
source,
|
|
3689
|
-
entry.bodyLimit ??
|
|
3770
|
+
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3771
|
+
this.protoPoisoning,
|
|
3690
3772
|
(parsed) => this.finishBodyOnly(entry, parsed, ctx, finalize, wrapResponse),
|
|
3691
3773
|
wrapResponse,
|
|
3692
3774
|
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
3693
3775
|
)
|
|
3694
3776
|
}
|
|
3695
3777
|
|
|
3696
|
-
/** Shared bounded body framing/parser. Both body lanes use the same trust-boundary checks; only
|
|
3697
|
-
* the validation + handler continuation differs. */
|
|
3698
|
-
private readBodyInput<T>(
|
|
3699
|
-
source: RequestSource,
|
|
3700
|
-
maxBodyBytes: number,
|
|
3701
|
-
onParsed: (parsed: unknown) => MaybePromise<T>,
|
|
3702
|
-
wrapResponse: (response: Response) => T,
|
|
3703
|
-
onError: (err: unknown) => MaybePromise<T>,
|
|
3704
|
-
): Promise<T> {
|
|
3705
|
-
const contentType = headerOf(source, "content-type") ?? ""
|
|
3706
|
-
if (contentType !== "application/json" && !contentType.includes("application/json")) {
|
|
3707
|
-
if (isUrlEncodedForm(contentType)) {
|
|
3708
|
-
return readBoundedForm(source, maxBodyBytes).then(
|
|
3709
|
-
(form) => (form instanceof Response ? wrapResponse(form) : onParsed(form)),
|
|
3710
|
-
onError,
|
|
3711
|
-
) as Promise<T>
|
|
3712
|
-
}
|
|
3713
|
-
return Promise.resolve(wrapResponse(jsonError(415, "unsupported_media_type")))
|
|
3714
|
-
}
|
|
3715
|
-
|
|
3716
|
-
// All JSON bodies go through readBoundedJson - the single enforcement point for the framed
|
|
3717
|
-
// fast path (post-read byte re-check), the streaming cap, and the prototype-poisoning guard.
|
|
3718
|
-
// A separate inlined native-json() fast path here would fork the trust boundary in two.
|
|
3719
|
-
try {
|
|
3720
|
-
return this.readBoundedJson(
|
|
3721
|
-
source,
|
|
3722
|
-
maxBodyBytes,
|
|
3723
|
-
(parsed) => (parsed instanceof Response ? wrapResponse(parsed) : onParsed(parsed)),
|
|
3724
|
-
onError,
|
|
3725
|
-
)
|
|
3726
|
-
} catch (err) {
|
|
3727
|
-
return Promise.resolve(onError(err))
|
|
3728
|
-
}
|
|
3729
|
-
}
|
|
3730
|
-
|
|
3731
3778
|
/** Validate + run the handler for the bodyOnly path - shared by the inline fast path and the
|
|
3732
3779
|
* streaming fallback. A method (not per-request closures) so the hot path allocates nothing
|
|
3733
3780
|
* beyond the one `.then` continuation. */
|
|
@@ -3736,7 +3783,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3736
3783
|
parsed: unknown,
|
|
3737
3784
|
ctx: RawContext,
|
|
3738
3785
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3739
|
-
wrapResponse: (response: Response) => T,
|
|
3786
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3740
3787
|
): MaybePromise<T> {
|
|
3741
3788
|
try {
|
|
3742
3789
|
const bodySchema = entry.schema!.body!
|
|
@@ -3771,7 +3818,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3771
3818
|
entry: RouteEntry,
|
|
3772
3819
|
ctx: RawContext,
|
|
3773
3820
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3774
|
-
wrapResponse?: (response: Response) => T,
|
|
3821
|
+
wrapResponse?: (response: Response | ResponseResult) => T,
|
|
3775
3822
|
): MaybePromise<T> {
|
|
3776
3823
|
if (entry.hasDecorations) Object.assign(ctx, entry.decorations)
|
|
3777
3824
|
const handlerOutput = entry.handler(ctx)
|
|
@@ -3794,7 +3841,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3794
3841
|
recovery: unknown,
|
|
3795
3842
|
ctx: RawContext,
|
|
3796
3843
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3797
|
-
wrapResponse: (response: Response) => T,
|
|
3844
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3798
3845
|
originalIssues: ReadonlyArray<StandardIssue>,
|
|
3799
3846
|
kind: "body" | "query",
|
|
3800
3847
|
): MaybePromise<T> {
|
|
@@ -3806,12 +3853,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3806
3853
|
const validation = entry.schema.body["~standard"].validate(recovery)
|
|
3807
3854
|
if (validation instanceof Promise) {
|
|
3808
3855
|
return validation.then((settled) => {
|
|
3809
|
-
if (settled.issues !== undefined)
|
|
3856
|
+
if (settled.issues !== undefined)
|
|
3857
|
+
return wrapResponse(plainValidationError(settled.issues))
|
|
3810
3858
|
ctx.body = settled.value
|
|
3811
3859
|
return this.executeHandler(entry, ctx, finalize)
|
|
3812
3860
|
})
|
|
3813
3861
|
}
|
|
3814
|
-
if (validation.issues !== undefined)
|
|
3862
|
+
if (validation.issues !== undefined)
|
|
3863
|
+
return wrapResponse(plainValidationError(validation.issues))
|
|
3815
3864
|
ctx.body = validation.value
|
|
3816
3865
|
return this.executeHandler(entry, ctx, finalize)
|
|
3817
3866
|
}
|
|
@@ -3820,19 +3869,21 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3820
3869
|
if (validation instanceof Promise) {
|
|
3821
3870
|
return validation.then(
|
|
3822
3871
|
(settled) => {
|
|
3823
|
-
if (settled.issues !== undefined)
|
|
3872
|
+
if (settled.issues !== undefined)
|
|
3873
|
+
return wrapResponse(plainValidationError(settled.issues))
|
|
3824
3874
|
ctx.query = settled.value
|
|
3825
3875
|
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
3826
3876
|
},
|
|
3827
3877
|
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
3828
3878
|
)
|
|
3829
3879
|
}
|
|
3830
|
-
if (validation.issues !== undefined)
|
|
3880
|
+
if (validation.issues !== undefined)
|
|
3881
|
+
return wrapResponse(plainValidationError(validation.issues))
|
|
3831
3882
|
ctx.query = validation.value
|
|
3832
3883
|
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
3833
3884
|
}
|
|
3834
3885
|
}
|
|
3835
|
-
return wrapResponse(
|
|
3886
|
+
return wrapResponse(plainValidationError(originalIssues))
|
|
3836
3887
|
}
|
|
3837
3888
|
|
|
3838
3889
|
private applyBodyValidation<T>(
|
|
@@ -3840,7 +3891,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3840
3891
|
result: StandardResult<unknown>,
|
|
3841
3892
|
ctx: RawContext,
|
|
3842
3893
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3843
|
-
wrapResponse: (response: Response) => T,
|
|
3894
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3844
3895
|
): MaybePromise<T> {
|
|
3845
3896
|
if (result.issues !== undefined) {
|
|
3846
3897
|
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError
|
|
@@ -3869,7 +3920,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3869
3920
|
"body",
|
|
3870
3921
|
)
|
|
3871
3922
|
}
|
|
3872
|
-
return wrapResponse(
|
|
3923
|
+
return wrapResponse(plainValidationError(result.issues))
|
|
3873
3924
|
}
|
|
3874
3925
|
ctx.body = result.value
|
|
3875
3926
|
return this.executeHandler(entry, ctx, finalize)
|
|
@@ -3881,7 +3932,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3881
3932
|
entry: RouteEntry,
|
|
3882
3933
|
ctx: RawContext,
|
|
3883
3934
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3884
|
-
wrapResponse: (response: Response) => T,
|
|
3935
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3885
3936
|
): MaybePromise<T> {
|
|
3886
3937
|
try {
|
|
3887
3938
|
// Call the validator directly for the raw StandardResult (read `.issues`/`.value`) - skip
|
|
@@ -3919,7 +3970,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3919
3970
|
result: StandardResult<unknown>,
|
|
3920
3971
|
ctx: RawContext,
|
|
3921
3972
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
3922
|
-
wrapResponse: (response: Response) => T,
|
|
3973
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
3923
3974
|
): MaybePromise<T> {
|
|
3924
3975
|
if (result.issues !== undefined) {
|
|
3925
3976
|
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError
|
|
@@ -3948,7 +3999,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
3948
3999
|
"query",
|
|
3949
4000
|
)
|
|
3950
4001
|
}
|
|
3951
|
-
return wrapResponse(
|
|
4002
|
+
return wrapResponse(plainValidationError(result.issues))
|
|
3952
4003
|
}
|
|
3953
4004
|
ctx.query = result.value
|
|
3954
4005
|
return this.executeHandler(entry, ctx, finalize, wrapResponse)
|
|
@@ -4090,7 +4141,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4090
4141
|
source: RequestSource,
|
|
4091
4142
|
ctx: RawContext,
|
|
4092
4143
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4093
|
-
wrapResponse: (response: Response) => T,
|
|
4144
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4094
4145
|
): MaybePromise<T> {
|
|
4095
4146
|
try {
|
|
4096
4147
|
const headersSchema = entry.schema?.headers
|
|
@@ -4163,7 +4214,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4163
4214
|
source: RequestSource,
|
|
4164
4215
|
ctx: RawContext,
|
|
4165
4216
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4166
|
-
wrapResponse: (response: Response) => T,
|
|
4217
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4167
4218
|
result: StandardResult<unknown>,
|
|
4168
4219
|
): MaybePromise<T> {
|
|
4169
4220
|
try {
|
|
@@ -4190,7 +4241,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4190
4241
|
source: RequestSource,
|
|
4191
4242
|
ctx: RawContext,
|
|
4192
4243
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4193
|
-
wrapResponse: (response: Response) => T,
|
|
4244
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4194
4245
|
): MaybePromise<T> {
|
|
4195
4246
|
const paramsSchema = entry.schema?.params
|
|
4196
4247
|
if (paramsSchema === undefined) {
|
|
@@ -4221,7 +4272,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4221
4272
|
entry: RouteEntry,
|
|
4222
4273
|
ctx: RawContext,
|
|
4223
4274
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4224
|
-
wrapResponse: (response: Response) => T,
|
|
4275
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4225
4276
|
): MaybePromise<T> {
|
|
4226
4277
|
try {
|
|
4227
4278
|
// The execution plan only selects this runner when the route has a query schema. Keeping the
|
|
@@ -4255,7 +4306,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4255
4306
|
source: RequestSource,
|
|
4256
4307
|
ctx: RawContext,
|
|
4257
4308
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4258
|
-
wrapResponse: (response: Response) => T,
|
|
4309
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4259
4310
|
): MaybePromise<T> {
|
|
4260
4311
|
return this.readAndValidateBody(source, entry, ctx).then(
|
|
4261
4312
|
(bodyError) =>
|
|
@@ -4274,7 +4325,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4274
4325
|
source: RequestSource,
|
|
4275
4326
|
ctx: RawContext,
|
|
4276
4327
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4277
|
-
wrapResponse: (response: Response) => T,
|
|
4328
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4278
4329
|
): MaybePromise<T> {
|
|
4279
4330
|
return this.readAndValidateBody(source, entry, ctx).then(
|
|
4280
4331
|
(bodyError) =>
|
|
@@ -4290,7 +4341,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4290
4341
|
entry: RouteEntry,
|
|
4291
4342
|
ctx: RawContext,
|
|
4292
4343
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4293
|
-
wrapResponse: (response: Response) => T,
|
|
4344
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4294
4345
|
): MaybePromise<T> {
|
|
4295
4346
|
const querySchema = entry.schema?.query
|
|
4296
4347
|
return querySchema === undefined
|
|
@@ -4302,7 +4353,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4302
4353
|
entry: RouteEntry,
|
|
4303
4354
|
ctx: RawContext,
|
|
4304
4355
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4305
|
-
wrapResponse: (response: Response) => T,
|
|
4356
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4306
4357
|
result: StandardResult<unknown>,
|
|
4307
4358
|
): MaybePromise<T> {
|
|
4308
4359
|
const queryError = this.applyLifecycleValidation(entry, result, ctx, "query")
|
|
@@ -4324,7 +4375,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4324
4375
|
source: RequestSource,
|
|
4325
4376
|
ctx: RawContext,
|
|
4326
4377
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4327
|
-
wrapResponse: (response: Response) => T,
|
|
4378
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4328
4379
|
result: StandardResult<unknown>,
|
|
4329
4380
|
): MaybePromise<T> {
|
|
4330
4381
|
try {
|
|
@@ -4346,8 +4397,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4346
4397
|
source: RequestSource,
|
|
4347
4398
|
ctx: RawContext,
|
|
4348
4399
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4349
|
-
wrapResponse: (response: Response) => T,
|
|
4350
|
-
paramsError: MaybePromise<Response | undefined>,
|
|
4400
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4401
|
+
paramsError: MaybePromise<Response | ResponseResult | undefined>,
|
|
4351
4402
|
): MaybePromise<T> {
|
|
4352
4403
|
if (paramsError instanceof Promise) {
|
|
4353
4404
|
return paramsError.then(
|
|
@@ -4371,8 +4422,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4371
4422
|
source: RequestSource,
|
|
4372
4423
|
ctx: RawContext,
|
|
4373
4424
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4374
|
-
wrapResponse: (response: Response) => T,
|
|
4375
|
-
paramsError: Response | undefined,
|
|
4425
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4426
|
+
paramsError: Response | ResponseResult | undefined,
|
|
4376
4427
|
): MaybePromise<T> {
|
|
4377
4428
|
if (paramsError !== undefined) return wrapResponse(paramsError)
|
|
4378
4429
|
if (entry.schema?.body !== undefined) {
|
|
@@ -4388,8 +4439,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4388
4439
|
entry: RouteEntry,
|
|
4389
4440
|
ctx: RawContext,
|
|
4390
4441
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4391
|
-
wrapResponse: (response: Response) => T,
|
|
4392
|
-
bodyError: Response | undefined,
|
|
4442
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4443
|
+
bodyError: Response | ResponseResult | undefined,
|
|
4393
4444
|
): MaybePromise<T> {
|
|
4394
4445
|
if (bodyError !== undefined) return wrapResponse(bodyError)
|
|
4395
4446
|
const querySchema = entry.schema?.query
|
|
@@ -4408,7 +4459,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4408
4459
|
entry: RouteEntry,
|
|
4409
4460
|
ctx: RawContext,
|
|
4410
4461
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4411
|
-
wrapResponse: (response: Response) => T,
|
|
4462
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4412
4463
|
result: StandardResult<unknown>,
|
|
4413
4464
|
): MaybePromise<T> {
|
|
4414
4465
|
try {
|
|
@@ -4429,8 +4480,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4429
4480
|
entry: RouteEntry,
|
|
4430
4481
|
ctx: RawContext,
|
|
4431
4482
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4432
|
-
wrapResponse: (response: Response) => T,
|
|
4433
|
-
queryError: Response | undefined,
|
|
4483
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4484
|
+
queryError: Response | ResponseResult | undefined,
|
|
4434
4485
|
): MaybePromise<T> {
|
|
4435
4486
|
if (queryError !== undefined) return wrapResponse(queryError)
|
|
4436
4487
|
return this.runLifecycleHooks(entry, ctx, finalize, wrapResponse)
|
|
@@ -4440,7 +4491,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4440
4491
|
entry: RouteEntry,
|
|
4441
4492
|
ctx: RawContext,
|
|
4442
4493
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4443
|
-
wrapResponse: (response: Response) => T,
|
|
4494
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4444
4495
|
): MaybePromise<T> {
|
|
4445
4496
|
if (entry.lifecycleHookLane === "derive-before") {
|
|
4446
4497
|
return this.runLifecycleDeriveBefore(entry, ctx, finalize, wrapResponse)
|
|
@@ -4452,6 +4503,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4452
4503
|
if (extension instanceof Promise) {
|
|
4453
4504
|
return this.continueLifecycleAfterDerive(entry, ctx, finalize, wrapResponse, i, extension)
|
|
4454
4505
|
}
|
|
4506
|
+
if (isDeriveEarlyExit(extension)) return finalize(extension, responseSet(ctx))
|
|
4455
4507
|
Object.assign(ctx, extension)
|
|
4456
4508
|
}
|
|
4457
4509
|
for (let i = 0; i < entry.beforeHandle.length; i++) {
|
|
@@ -4479,13 +4531,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4479
4531
|
entry: RouteEntry,
|
|
4480
4532
|
ctx: RawContext,
|
|
4481
4533
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4482
|
-
wrapResponse: (response: Response) => T,
|
|
4534
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4483
4535
|
): MaybePromise<T> {
|
|
4484
4536
|
try {
|
|
4485
4537
|
const extension = entry.derives[0]!(ctx)
|
|
4486
4538
|
if (extension instanceof Promise) {
|
|
4487
4539
|
return this.continueLifecycleAfterDerive(entry, ctx, finalize, wrapResponse, 0, extension)
|
|
4488
4540
|
}
|
|
4541
|
+
if (isDeriveEarlyExit(extension)) return finalize(extension, responseSet(ctx))
|
|
4489
4542
|
Object.assign(ctx, extension)
|
|
4490
4543
|
|
|
4491
4544
|
const outcome = entry.beforeHandle[0]!(ctx)
|
|
@@ -4513,7 +4566,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4513
4566
|
entry: RouteEntry,
|
|
4514
4567
|
ctx: RawContext,
|
|
4515
4568
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4516
|
-
wrapResponse: (response: Response) => T,
|
|
4569
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4517
4570
|
result: unknown,
|
|
4518
4571
|
): MaybePromise<T> {
|
|
4519
4572
|
try {
|
|
@@ -4527,15 +4580,19 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4527
4580
|
entry: RouteEntry,
|
|
4528
4581
|
ctx: RawContext,
|
|
4529
4582
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4530
|
-
wrapResponse: (response: Response) => T,
|
|
4583
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4531
4584
|
firstIndex: number,
|
|
4532
4585
|
first: Promise<unknown>,
|
|
4533
4586
|
): Promise<T> {
|
|
4534
4587
|
try {
|
|
4535
|
-
|
|
4588
|
+
const settled = await first
|
|
4589
|
+
if (isDeriveEarlyExit(settled)) return finalize(settled, responseSet(ctx))
|
|
4590
|
+
Object.assign(ctx, settled)
|
|
4536
4591
|
for (let i = firstIndex + 1; i < entry.derives.length; i++) {
|
|
4537
|
-
const
|
|
4538
|
-
|
|
4592
|
+
const raw = entry.derives[i]!(ctx)
|
|
4593
|
+
const extension = raw instanceof Promise ? await raw : raw
|
|
4594
|
+
if (isDeriveEarlyExit(extension)) return finalize(extension, responseSet(ctx))
|
|
4595
|
+
Object.assign(ctx, extension)
|
|
4539
4596
|
}
|
|
4540
4597
|
return await this.runLifecycleHooksAsync(entry, ctx, finalize, wrapResponse)
|
|
4541
4598
|
} catch (err) {
|
|
@@ -4547,7 +4604,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4547
4604
|
entry: RouteEntry,
|
|
4548
4605
|
ctx: RawContext,
|
|
4549
4606
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4550
|
-
wrapResponse: (response: Response) => T,
|
|
4607
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4551
4608
|
): Promise<T> {
|
|
4552
4609
|
try {
|
|
4553
4610
|
// Indexed, not `for...of`. An array iterator declared inside an async function stays live
|
|
@@ -4569,7 +4626,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4569
4626
|
entry: RouteEntry,
|
|
4570
4627
|
ctx: RawContext,
|
|
4571
4628
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4572
|
-
wrapResponse: (response: Response) => T,
|
|
4629
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4573
4630
|
firstIndex: number,
|
|
4574
4631
|
first: Promise<unknown>,
|
|
4575
4632
|
): Promise<T> {
|
|
@@ -4591,7 +4648,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4591
4648
|
entry: RouteEntry,
|
|
4592
4649
|
ctx: RawContext,
|
|
4593
4650
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4594
|
-
wrapResponse: (response: Response) => T,
|
|
4651
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4595
4652
|
): Promise<T> {
|
|
4596
4653
|
try {
|
|
4597
4654
|
const output = entry.handler(ctx)
|
|
@@ -4606,7 +4663,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4606
4663
|
entry: RouteEntry,
|
|
4607
4664
|
ctx: RawContext,
|
|
4608
4665
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4609
|
-
wrapResponse: (response: Response) => T,
|
|
4666
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4610
4667
|
initial: unknown,
|
|
4611
4668
|
): MaybePromise<T> {
|
|
4612
4669
|
try {
|
|
@@ -4635,7 +4692,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4635
4692
|
entry: RouteEntry,
|
|
4636
4693
|
ctx: RawContext,
|
|
4637
4694
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4638
|
-
wrapResponse: (response: Response) => T,
|
|
4695
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4639
4696
|
firstIndex: number,
|
|
4640
4697
|
first: Promise<unknown>,
|
|
4641
4698
|
): Promise<T> {
|
|
@@ -4655,7 +4712,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4655
4712
|
entry: RouteEntry,
|
|
4656
4713
|
ctx: RawContext,
|
|
4657
4714
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4658
|
-
wrapResponse: (response: Response) => T,
|
|
4715
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4659
4716
|
result: unknown,
|
|
4660
4717
|
): MaybePromise<T> {
|
|
4661
4718
|
try {
|
|
@@ -4677,7 +4734,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4677
4734
|
private finishContractOutcome<T>(
|
|
4678
4735
|
ctx: RawContext,
|
|
4679
4736
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4680
|
-
wrapResponse: (response: Response) => T,
|
|
4737
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4681
4738
|
outcome: {
|
|
4682
4739
|
readonly kind: "ok" | "warn" | "violation"
|
|
4683
4740
|
readonly value?: unknown
|
|
@@ -4690,7 +4747,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4690
4747
|
path: pathnameOf(ctx.req.url),
|
|
4691
4748
|
detail: outcome.message,
|
|
4692
4749
|
})
|
|
4693
|
-
return wrapResponse(
|
|
4750
|
+
return wrapResponse(plainError(500, "internal_error"))
|
|
4694
4751
|
}
|
|
4695
4752
|
if (outcome.kind === "warn") {
|
|
4696
4753
|
this.logger.warn("response contract", {
|
|
@@ -4702,19 +4759,44 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4702
4759
|
return finalize(outcome.value, responseSet(ctx))
|
|
4703
4760
|
}
|
|
4704
4761
|
|
|
4705
|
-
|
|
4762
|
+
/** Synchronous by design, and `MaybePromise` rather than `Promise` for the same reason: control flow
|
|
4763
|
+
* out of a lifecycle stage must not cost a promise. An `async` method allocates one and defers the
|
|
4764
|
+
* result a microtask even when every branch it takes returns immediately, which is the common case
|
|
4765
|
+
* here - a thrown `status(...)`/`Response`, or a route with no `onError` hook at all. Only the hook
|
|
4766
|
+
* loop, which may await, drops into {@link runErrorHooks}. */
|
|
4767
|
+
private handleLifecycleError<T>(
|
|
4706
4768
|
entry: RouteEntry,
|
|
4707
4769
|
err: unknown,
|
|
4708
4770
|
ctx: RawContext,
|
|
4709
4771
|
finalize: (result: unknown, set: CtxSet) => T,
|
|
4710
|
-
wrapResponse: (response: Response) => T,
|
|
4711
|
-
):
|
|
4772
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4773
|
+
): MaybePromise<T> {
|
|
4712
4774
|
// A *thrown* Response is deliberate control flow, not an error - a guard throws a redirect/401,
|
|
4713
4775
|
// an action throws an error page. Return it as-is (Remix/SvelteKit semantics); don't run onError
|
|
4714
4776
|
// or log it as a 500. This is what makes `throw redirect(...)` / `requireSession(...)` work from
|
|
4715
4777
|
// any handler or loader.
|
|
4716
4778
|
if (err instanceof Response) return wrapResponse(err)
|
|
4717
|
-
//
|
|
4779
|
+
// Same rule for a thrown `status(...)`, but through `finalize` rather than `wrapResponse`: the
|
|
4780
|
+
// value is still plain data, so the ordinary JSON lane renders it and no `Response` is built.
|
|
4781
|
+
if (isResponseResult(err)) return finalize(err, responseSet(ctx))
|
|
4782
|
+
if (entry.onError.length === 0) {
|
|
4783
|
+
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
4784
|
+
// (redacting) logger. Body-read failures and around-hook failures land here too.
|
|
4785
|
+
this.logRequestError(err, ctx)
|
|
4786
|
+
return wrapResponse(plainError(500, "internal_error"))
|
|
4787
|
+
}
|
|
4788
|
+
return this.runErrorHooks(entry, err, ctx, finalize, wrapResponse)
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
/** The `onError` chain, split off {@link handleLifecycleError} so its `await` costs only the routes
|
|
4792
|
+
* that registered a hook. A hook may return a custom response; otherwise the default 500 stands. */
|
|
4793
|
+
private async runErrorHooks<T>(
|
|
4794
|
+
entry: RouteEntry,
|
|
4795
|
+
err: unknown,
|
|
4796
|
+
ctx: RawContext,
|
|
4797
|
+
finalize: (result: unknown, set: CtxSet) => T,
|
|
4798
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
4799
|
+
): Promise<T> {
|
|
4718
4800
|
// Indexed for the same reason as the `beforeHandle` chain above: the iterator would outlive
|
|
4719
4801
|
// the `await` and stay heap-allocated on both engines.
|
|
4720
4802
|
for (let i = 0; i < entry.onError.length; i++) {
|
|
@@ -4722,51 +4804,35 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4722
4804
|
const handled = outcome instanceof Promise ? await outcome : outcome
|
|
4723
4805
|
if (handled !== undefined) return finalize(handled, responseSet(ctx))
|
|
4724
4806
|
}
|
|
4725
|
-
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
4726
|
-
// (redacting) logger. Body-read failures and around-hook failures land here too.
|
|
4727
4807
|
this.logRequestError(err, ctx)
|
|
4728
|
-
return wrapResponse(
|
|
4808
|
+
return wrapResponse(plainError(500, "internal_error"))
|
|
4729
4809
|
}
|
|
4730
4810
|
|
|
4731
4811
|
/** Log an unhandled request error to the (redacting) logger - shared by {@link runLifecycle} and the
|
|
4732
4812
|
* bare fast path ({@link bareError}) so both record the same fields. Never throws; never leaks. */
|
|
4733
4813
|
private logRequestError(err: unknown, ctx: RawContext): void {
|
|
4734
|
-
|
|
4735
|
-
// the app's call (`errorLogDetail`). The default keeps it: a 500 with no message and no stack is
|
|
4736
|
-
// an incident nobody can diagnose, and the framework already ships the narrower instrument for
|
|
4737
|
-
// the leak - a redacting logger with `valuePatterns`. `"none"` is there for an untrusted sink.
|
|
4738
|
-
const detail = this.errorLogDetail
|
|
4739
|
-
this.logger.error("unhandled request error", {
|
|
4740
|
-
method: ctx.req.method,
|
|
4741
|
-
path: pathnameOf(ctx.req.url),
|
|
4742
|
-
name: err instanceof Error ? err.name : "Error",
|
|
4743
|
-
// `detail`, not `message`: the logger uses `message` for its own first argument, so a field of
|
|
4744
|
-
// that name is silently overwritten and the thrown error's own text never reaches the sink. It
|
|
4745
|
-
// survived only incidentally inside `stack`, and was lost outright for a non-Error throw.
|
|
4746
|
-
...(detail === "none" ? {} : { detail: err instanceof Error ? err.message : String(err) }),
|
|
4747
|
-
...(detail === "full" && err instanceof Error ? { stack: err.stack } : {}),
|
|
4748
|
-
})
|
|
4814
|
+
emitRequestErrorLog(this.logger, this.errorLogDetail, err, ctx)
|
|
4749
4815
|
}
|
|
4750
4816
|
|
|
4751
4817
|
private async readAndValidateBody(
|
|
4752
4818
|
req: RequestSource,
|
|
4753
4819
|
entry: RouteEntry,
|
|
4754
4820
|
ctx: RawContext,
|
|
4755
|
-
): Promise<Response | undefined> {
|
|
4821
|
+
): Promise<Response | ResponseResult | undefined> {
|
|
4756
4822
|
const contentType = headerOf(req, "content-type") ?? ""
|
|
4757
4823
|
let parsed: unknown
|
|
4758
4824
|
if (contentType === "application/json" || contentType.includes("application/json")) {
|
|
4759
|
-
const json = await this.readBoundedJson(req, entry.bodyLimit ??
|
|
4760
|
-
if (json
|
|
4825
|
+
const json = await this.readBoundedJson(req, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
4826
|
+
if (isResponseResult(json)) return json
|
|
4761
4827
|
parsed = json
|
|
4762
4828
|
} else if (isUrlEncodedForm(contentType)) {
|
|
4763
|
-
const form = await readBoundedForm(req, entry.bodyLimit ??
|
|
4764
|
-
if (form
|
|
4829
|
+
const form = await readBoundedForm(req, entry.bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
4830
|
+
if (isResponseResult(form)) return form
|
|
4765
4831
|
parsed = form
|
|
4766
4832
|
} else {
|
|
4767
4833
|
// multipart/form-data (file uploads) stays 415 on the schema path by design - files don't
|
|
4768
4834
|
// fit a value schema; use a schema-less route + @nifrajs/uploads helpers for those.
|
|
4769
|
-
return
|
|
4835
|
+
return plainError(415, "unsupported_media_type")
|
|
4770
4836
|
}
|
|
4771
4837
|
const validation = entry.schema!.body!["~standard"].validate(parsed)
|
|
4772
4838
|
const result = validation instanceof Promise ? await validation : validation
|
|
@@ -4780,7 +4846,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4780
4846
|
result: StandardResult<unknown>,
|
|
4781
4847
|
ctx: RawContext,
|
|
4782
4848
|
kind: "body" | "query" | "params" | "headers",
|
|
4783
|
-
): MaybePromise<Response | undefined> {
|
|
4849
|
+
): MaybePromise<Response | ResponseResult | undefined> {
|
|
4784
4850
|
const assign = (value: unknown): void => {
|
|
4785
4851
|
if (kind === "body") ctx.body = value
|
|
4786
4852
|
else if (kind === "query") ctx.query = value
|
|
@@ -4792,7 +4858,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4792
4858
|
return undefined
|
|
4793
4859
|
}
|
|
4794
4860
|
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError
|
|
4795
|
-
if (hook === undefined) return
|
|
4861
|
+
if (hook === undefined) return plainValidationError(result.issues)
|
|
4796
4862
|
const attempted = hook(result.issues, ctx as unknown as Context, kind)
|
|
4797
4863
|
if (attempted instanceof Promise) {
|
|
4798
4864
|
return attempted.then((recovery) =>
|
|
@@ -4808,8 +4874,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4808
4874
|
issues: ReadonlyArray<StandardIssue>,
|
|
4809
4875
|
recovery: unknown,
|
|
4810
4876
|
assign: (value: unknown) => void,
|
|
4811
|
-
): MaybePromise<Response | undefined> {
|
|
4812
|
-
if (recovery === undefined) return
|
|
4877
|
+
): MaybePromise<Response | ResponseResult | undefined> {
|
|
4878
|
+
if (recovery === undefined) return plainValidationError(issues)
|
|
4813
4879
|
if (recovery instanceof Response) return recovery
|
|
4814
4880
|
const schema =
|
|
4815
4881
|
kind === "body"
|
|
@@ -4822,12 +4888,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4822
4888
|
const retried = schema!["~standard"].validate(recovery)
|
|
4823
4889
|
if (retried instanceof Promise) {
|
|
4824
4890
|
return retried.then((settled) => {
|
|
4825
|
-
if (settled.issues !== undefined) return
|
|
4891
|
+
if (settled.issues !== undefined) return plainValidationError(settled.issues)
|
|
4826
4892
|
assign(settled.value)
|
|
4827
4893
|
return undefined
|
|
4828
4894
|
})
|
|
4829
4895
|
}
|
|
4830
|
-
if (retried.issues !== undefined) return
|
|
4896
|
+
if (retried.issues !== undefined) return plainValidationError(retried.issues)
|
|
4831
4897
|
assign(retried.value)
|
|
4832
4898
|
return undefined
|
|
4833
4899
|
}
|
|
@@ -4839,19 +4905,22 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4839
4905
|
* length-less body mid-stream once the running byte count exceeds the cap - so a lying
|
|
4840
4906
|
* or absent length can't force us to buffer an oversized payload.
|
|
4841
4907
|
*/
|
|
4842
|
-
private readBoundedJson(
|
|
4908
|
+
private readBoundedJson(
|
|
4909
|
+
req: RequestSource,
|
|
4910
|
+
maxBodyBytes?: number,
|
|
4911
|
+
): Promise<unknown | ResponseResult>
|
|
4843
4912
|
private readBoundedJson<T>(
|
|
4844
4913
|
req: RequestSource,
|
|
4845
4914
|
maxBodyBytes: number,
|
|
4846
|
-
onResult: (parsed: unknown |
|
|
4915
|
+
onResult: (parsed: unknown | ResponseResult) => T | Promise<T>,
|
|
4847
4916
|
onError: (error: unknown) => T | Promise<T>,
|
|
4848
4917
|
): Promise<T>
|
|
4849
4918
|
private readBoundedJson<T>(
|
|
4850
4919
|
req: RequestSource,
|
|
4851
4920
|
maxBodyBytes = this.maxBodyBytes,
|
|
4852
|
-
onResult?: (parsed: unknown |
|
|
4921
|
+
onResult?: (parsed: unknown | ResponseResult) => T | Promise<T>,
|
|
4853
4922
|
onError?: (error: unknown) => T | Promise<T>,
|
|
4854
|
-
): Promise<unknown |
|
|
4923
|
+
): Promise<unknown | ResponseResult | T> {
|
|
4855
4924
|
// Not `async`: this is a pass-through, and wrapping the lane's own promise in a coroutine
|
|
4856
4925
|
// frame costs an extra microtask hop on every JSON body.
|
|
4857
4926
|
return onResult === undefined
|