@nifrajs/core 2.13.0 → 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/binary.d.ts +32 -0
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +17 -0
- package/dist/binary.js.map +1 -1
- package/dist/durable-adapters.d.ts.map +1 -1
- package/dist/durable-adapters.js +21 -50
- package/dist/durable-adapters.js.map +1 -1
- package/dist/durable-execution.d.ts.map +1 -1
- package/dist/durable-execution.js +49 -69
- package/dist/durable-execution.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/durable-approval-protocol.d.ts +13 -0
- package/dist/internal/durable-approval-protocol.d.ts.map +1 -0
- package/dist/internal/durable-approval-protocol.js +66 -0
- package/dist/internal/durable-approval-protocol.js.map +1 -0
- package/dist/internal/durable-effect-protocol.d.ts +14 -0
- package/dist/internal/durable-effect-protocol.d.ts.map +1 -0
- package/dist/internal/durable-effect-protocol.js +16 -0
- package/dist/internal/durable-effect-protocol.js.map +1 -0
- package/dist/internal/durable-saga-protocol.d.ts +8 -0
- package/dist/internal/durable-saga-protocol.d.ts.map +1 -0
- package/dist/internal/durable-saga-protocol.js +21 -0
- package/dist/internal/durable-saga-protocol.js.map +1 -0
- package/dist/internal/route-catalog.d.ts +3 -0
- package/dist/internal/route-catalog.d.ts.map +1 -1
- package/dist/internal/route-catalog.js +5 -0
- package/dist/internal/route-catalog.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/plugin.d.ts +1 -1
- package/dist/server/plugin.d.ts.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/registry.d.ts +11 -2
- package/dist/server/registry.d.ts.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 +82 -45
- 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 +18 -1
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +89 -48
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +397 -276
- 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/binary.ts +35 -0
- package/src/durable-adapters.ts +26 -51
- package/src/durable-execution.ts +46 -88
- package/src/edge-kit.ts +27 -0
- package/src/errors.ts +1 -0
- package/src/index.ts +1 -1
- package/src/internal/durable-approval-protocol.ts +92 -0
- package/src/internal/durable-effect-protocol.ts +37 -0
- package/src/internal/durable-saga-protocol.ts +55 -0
- package/src/internal/route-catalog.ts +6 -0
- 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/plugin.ts +1 -1
- package/src/server/query.ts +10 -4
- package/src/server/registry.ts +19 -4
- package/src/server/request-context.ts +106 -51
- 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 +18 -1
- package/src/server/server.ts +607 -376
- 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
|
@@ -11,7 +11,14 @@ import {
|
|
|
11
11
|
rememberMutableHeaders,
|
|
12
12
|
taggedResponseBody,
|
|
13
13
|
} from "./respond.ts"
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
EMPTY_RESPONSE_CONTROLS,
|
|
16
|
+
type HandlerResult,
|
|
17
|
+
isResponseResult,
|
|
18
|
+
type PlainRender,
|
|
19
|
+
plainRenderHeaders,
|
|
20
|
+
type ResponseResult,
|
|
21
|
+
} from "./runtime-core.ts"
|
|
15
22
|
import type { CtxSet } from "./server.ts"
|
|
16
23
|
import {
|
|
17
24
|
mergeStaticHeaderRecord,
|
|
@@ -58,6 +65,8 @@ export type NodeServeOutcome =
|
|
|
58
65
|
*/
|
|
59
66
|
export function toNodeOutcome(result: HandlerResult, set: CtxSet): NodeServeOutcome {
|
|
60
67
|
if (isResponseResult(result)) {
|
|
68
|
+
const plain = result.plain
|
|
69
|
+
if (plain !== undefined) return plainNodeOutcome(plain, set)
|
|
61
70
|
const body = result.toNodeBody?.()
|
|
62
71
|
if (body !== undefined) {
|
|
63
72
|
return {
|
|
@@ -85,6 +94,24 @@ export function toNodeOutcome(result: HandlerResult, set: CtxSet): NodeServeOutc
|
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
/**
|
|
98
|
+
* A `status(...)` (or any other {@link PlainRender} carrier) on the node lane: the same `kind: "json"`
|
|
99
|
+
* outcome a handler's plain return produces, so an early exit is written to the socket by the same
|
|
100
|
+
* direct writer, with a `content-length`, and never builds a `Response`.
|
|
101
|
+
*/
|
|
102
|
+
export function plainNodeOutcome(plain: PlainRender, set: CtxSet): NodeServeOutcome {
|
|
103
|
+
return {
|
|
104
|
+
kind: "json",
|
|
105
|
+
status: plain.status,
|
|
106
|
+
headers: plainRenderHeaders(plain, set),
|
|
107
|
+
cookies: set._cookies,
|
|
108
|
+
body:
|
|
109
|
+
plain.body === undefined || isBodylessStatus(plain.status)
|
|
110
|
+
? null
|
|
111
|
+
: JSON.stringify(plain.body),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
/**
|
|
89
116
|
* Materialize a buffered node outcome only when a Web `onResponse` hook needs to see a real
|
|
90
117
|
* `Response`. The marker lets an in-place hook (`response.headers.set(...); return response`) go back
|
|
@@ -162,8 +189,22 @@ export function withStaticNodeHeaders(
|
|
|
162
189
|
return { ...outcome, headers }
|
|
163
190
|
}
|
|
164
191
|
|
|
165
|
-
|
|
166
|
-
|
|
192
|
+
/**
|
|
193
|
+
* The node lane's `wrapResponse`: what an early exit built OUTSIDE the handler's finalizer renders as
|
|
194
|
+
* - an `onRequest` hook's `Response`, a mount's, and every framework error render.
|
|
195
|
+
*
|
|
196
|
+
* A plain-data carrier ({@link plainError}, `status(...)`) takes the same `kind: "json"` lane a
|
|
197
|
+
* handler's plain return takes: no `Response` built, and none drained. `EMPTY_RESPONSE_CONTROLS`
|
|
198
|
+
* rather than the request's `c.set` on purpose - these renders happen where no context exists (before
|
|
199
|
+
* routing, or after it was abandoned), which is exactly why they are wrapped rather than finalized.
|
|
200
|
+
*/
|
|
201
|
+
export function nodeOutcomeFromResponse(result: Response | ResponseResult): NodeServeOutcome {
|
|
202
|
+
if (!(result instanceof Response)) {
|
|
203
|
+
const plain = result.plain
|
|
204
|
+
if (plain !== undefined) return plainNodeOutcome(plain, EMPTY_RESPONSE_CONTROLS)
|
|
205
|
+
return nodeOutcomeFromResponse(result.toResponse())
|
|
206
|
+
}
|
|
207
|
+
const response = normalizeBodylessResponse(result)
|
|
167
208
|
const body = nodeResponseBody(response)
|
|
168
209
|
return body === undefined
|
|
169
210
|
? { kind: "response", response }
|
package/src/server/plugin.ts
CHANGED
|
@@ -86,7 +86,7 @@ export interface PluginTypeCollapsed {
|
|
|
86
86
|
* every route type flowing through it is about to be widened away.
|
|
87
87
|
*/
|
|
88
88
|
// biome-ignore lint/suspicious/noExplicitAny: matches any Server shape to inspect its Registry alone
|
|
89
|
-
type ServerTypeUnpinned<S> = [S] extends [Server<infer R, any>]
|
|
89
|
+
export type ServerTypeUnpinned<S> = [S] extends [Server<infer R, any>]
|
|
90
90
|
? 0 extends 1 & R
|
|
91
91
|
? true
|
|
92
92
|
: false
|
package/src/server/query.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* and the byte-capped HTML-form reader. Split out of the server kernel so the file stays one domain.
|
|
5
5
|
*/
|
|
6
6
|
import { readBoundedBytes } from "./body.ts"
|
|
7
|
-
import {
|
|
7
|
+
import { plainError, urlPartsOf } from "./http.ts"
|
|
8
|
+
import type { ResponseResult } from "./runtime-core.ts"
|
|
8
9
|
import type { RequestSource } from "./server.ts"
|
|
9
10
|
|
|
10
11
|
// The query string ("?a=1", or "" when absent) - for lazily building `c.query` only when read.
|
|
@@ -123,16 +124,21 @@ const FORM_DECODER = new TextDecoder()
|
|
|
123
124
|
* Read an HTML-form body (urlencoded) into the plain object a body schema validates - same byte
|
|
124
125
|
* cap as the JSON path (a lying/absent Content-Length can't force an oversized buffer), same
|
|
125
126
|
* repeated-key -> `string[]` promotion as query parsing, same `__proto__` guard.
|
|
127
|
+
*
|
|
128
|
+
* A rejection is a `ResponseResult`, not a built `Response`, matching the JSON reader: the cap is a
|
|
129
|
+
* framework render, and answering one on the plain lane costs what a handler's plain return costs
|
|
130
|
+
* (see `plainError`). The distinction is observable on the Node adapter - a `Response` there is
|
|
131
|
+
* untagged, so its body drains through a Web stream and the answer goes out chunked.
|
|
126
132
|
*/
|
|
127
133
|
export async function readBoundedForm(
|
|
128
134
|
req: RequestSource,
|
|
129
135
|
maxBytes: number,
|
|
130
|
-
): Promise<Record<string, QueryValue> |
|
|
136
|
+
): Promise<Record<string, QueryValue> | ResponseResult> {
|
|
131
137
|
const read = await readBoundedBytes(req, maxBytes)
|
|
132
138
|
if (!read.ok) {
|
|
133
139
|
return read.status === 413
|
|
134
|
-
?
|
|
135
|
-
:
|
|
140
|
+
? plainError(413, "payload_too_large")
|
|
141
|
+
: plainError(400, "invalid_content_length")
|
|
136
142
|
}
|
|
137
143
|
const out: Record<string, QueryValue> = Object.create(null) as Record<string, QueryValue>
|
|
138
144
|
// URLSearchParams owns the format's quirks (`+` as space, percent-decoding, empty keys);
|
package/src/server/registry.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BinaryResponse, RawResponse } from "../binary.ts"
|
|
1
2
|
import type { InferInput, InferOutput, StandardSchemaV1 } from "../schema/standard.ts"
|
|
2
3
|
import type { Params, RouteSchema } from "./context.ts"
|
|
3
4
|
|
|
@@ -79,12 +80,26 @@ export type RouteInfoFor<Path extends string, S extends RouteSchema, Output> = {
|
|
|
79
80
|
readonly sse: RegistrySse<S>
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
/**
|
|
84
|
+
* The client-visible output of a handler: its awaited return, with an UNbranded raw `Response` removed
|
|
85
|
+
* (a route returning a bare `Response` has no describable body). A response branded by `bytes()`
|
|
86
|
+
* ({@link BinaryResponse}) or `raw()` ({@link RawResponse}) is KEPT, so the brand reaches the client's
|
|
87
|
+
* `Jsonify` and types `data` as `Blob` / `Jsonify<T>` rather than collapsing to `never`.
|
|
88
|
+
*/
|
|
89
|
+
export type OutputOf<H extends (...args: never[]) => unknown> = KeepBrandedResponse<
|
|
90
|
+
Awaited<ReturnType<H>>
|
|
86
91
|
>
|
|
87
92
|
|
|
93
|
+
/** Preserve a `bytes()`/`raw()`-branded response; drop only a bare `Response`. Distributes over a
|
|
94
|
+
* union return, so `Foo | BinaryResponse` keeps both arms. */
|
|
95
|
+
type KeepBrandedResponse<O> = O extends BinaryResponse
|
|
96
|
+
? O
|
|
97
|
+
: O extends RawResponse<unknown>
|
|
98
|
+
? O
|
|
99
|
+
: O extends Response
|
|
100
|
+
? never
|
|
101
|
+
: O
|
|
102
|
+
|
|
88
103
|
/**
|
|
89
104
|
* The registry entry for a WebSocket route (stored under the pseudo-method key `"WS"`, so WS routes
|
|
90
105
|
* ride the same path → method map as HTTP routes - no extra Server generic). `in` is the client →
|
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
import type { Platform } from "./context.ts"
|
|
17
17
|
import { type CookieOptions, cookieNamePrefix, parseCookies, serializeCookie } from "./cookies.ts"
|
|
18
18
|
import { headerObjectOf } from "./headers.ts"
|
|
19
|
-
import {
|
|
19
|
+
import { plainError } from "./http.ts"
|
|
20
20
|
import { guardParsedValue, type ProtoPoisoning, parseJsonGuarded } from "./proto-guard.ts"
|
|
21
|
-
import { searchOf } from "./query.ts"
|
|
21
|
+
import { isUrlEncodedForm, readBoundedForm, searchOf } from "./query.ts"
|
|
22
22
|
import { responseJsonContentType } from "./respond.ts"
|
|
23
23
|
import {
|
|
24
24
|
CONTEXT_SEARCH,
|
|
@@ -27,12 +27,14 @@ import {
|
|
|
27
27
|
getNeverAbortSignal,
|
|
28
28
|
getUnboundedRequestBudget,
|
|
29
29
|
headerOf,
|
|
30
|
+
isResponseResult,
|
|
30
31
|
PRE_DECODED_BODY,
|
|
31
32
|
type PreDecodedBody,
|
|
33
|
+
type ResponseResult,
|
|
32
34
|
requestOf,
|
|
33
35
|
TEXT_DECODER,
|
|
34
36
|
} from "./runtime-core.ts"
|
|
35
|
-
import type { CtxSet, RawContext, RequestSource } from "./server.ts"
|
|
37
|
+
import type { CtxSet, MaybePromise, RawContext, RequestSource } from "./server.ts"
|
|
36
38
|
|
|
37
39
|
/** A fixed past instant for cookie deletion (`Expires`). A literal epoch - deterministic, unlike an
|
|
38
40
|
* argless `new Date()`. */
|
|
@@ -390,7 +392,7 @@ export class RequestContext implements RawContext {
|
|
|
390
392
|
}
|
|
391
393
|
}
|
|
392
394
|
|
|
393
|
-
/** Backs `c.boundedBody`: bounded byte read that throws a flat 413/400
|
|
395
|
+
/** Backs `c.boundedBody`: bounded byte read that throws a flat 413/400 plain render (caught by
|
|
394
396
|
* `runLifecycle` as control flow, like `throw redirect(...)`), so a handler can't ignore the cap.
|
|
395
397
|
* The byte-cap itself lives in `./body.ts` (shared with the schema path and `verifyWebhook`). */
|
|
396
398
|
async function readBoundedBodyOrThrow(
|
|
@@ -401,8 +403,8 @@ async function readBoundedBodyOrThrow(
|
|
|
401
403
|
const r = await readBoundedBytes(req, maxBytes ?? maxBodyBytes)
|
|
402
404
|
if (r.ok) return r.bytes
|
|
403
405
|
throw r.status === 413
|
|
404
|
-
?
|
|
405
|
-
:
|
|
406
|
+
? plainError(413, "payload_too_large")
|
|
407
|
+
: plainError(400, "invalid_content_length")
|
|
406
408
|
}
|
|
407
409
|
|
|
408
410
|
/** Backs `c.boundedJson`: `readBoundedJsonSource`, throwing its flat 400/413 on failure. */
|
|
@@ -413,7 +415,9 @@ async function readBoundedJsonBodyOrThrow<T>(
|
|
|
413
415
|
maxBytes?: number,
|
|
414
416
|
): Promise<T> {
|
|
415
417
|
const parsed = await readBoundedJsonSource(req, maxBytes ?? maxBodyBytes, protoPoisoning)
|
|
416
|
-
|
|
418
|
+
// The lane's own failures are the only branded values it can produce: a parsed JSON body is
|
|
419
|
+
// `JSON.parse` output, which carries string keys only and so can never answer to a symbol brand.
|
|
420
|
+
if (isResponseResult(parsed)) throw parsed
|
|
417
421
|
return parsed as T
|
|
418
422
|
}
|
|
419
423
|
|
|
@@ -431,7 +435,7 @@ async function readBoundedJsonBodyOrThrow<T>(
|
|
|
431
435
|
* own promise chain instead, and only the streaming fallback - which is already doing IO in a loop
|
|
432
436
|
* - runs as an async function. Callers that `await` the result are unaffected.
|
|
433
437
|
*/
|
|
434
|
-
type JsonResultContinuation<T> = (result: unknown |
|
|
438
|
+
type JsonResultContinuation<T> = (result: unknown | ResponseResult) => T | Promise<T>
|
|
435
439
|
type JsonErrorContinuation<T> = (error: unknown) => T | Promise<T>
|
|
436
440
|
type FramedJsonSource = RequestSource & {
|
|
437
441
|
readonly bytes?: () => Promise<Uint8Array>
|
|
@@ -445,7 +449,7 @@ export function readBoundedJsonSource(
|
|
|
445
449
|
req: RequestSource,
|
|
446
450
|
maxBytes: number,
|
|
447
451
|
protoPoisoning?: ProtoPoisoning,
|
|
448
|
-
): Promise<unknown |
|
|
452
|
+
): Promise<unknown | ResponseResult>
|
|
449
453
|
export function readBoundedJsonSource<T>(
|
|
450
454
|
req: RequestSource,
|
|
451
455
|
maxBytes: number,
|
|
@@ -459,7 +463,7 @@ export function readBoundedJsonSource<T>(
|
|
|
459
463
|
protoPoisoning: ProtoPoisoning = "reject",
|
|
460
464
|
onResult?: JsonResultContinuation<T>,
|
|
461
465
|
onError?: JsonErrorContinuation<T>,
|
|
462
|
-
): Promise<unknown |
|
|
466
|
+
): Promise<unknown | ResponseResult | T> {
|
|
463
467
|
// A pre-parsing hook (the transport-codec lane) may have decoded the body already - its stash
|
|
464
468
|
// is taken verbatim: the stasher enforced its own byte cap and poisoning policy on text this
|
|
465
469
|
// lane never sees. One symbol read; a miss on ordinary requests costs a cache-line, not a parse.
|
|
@@ -493,53 +497,56 @@ export function readBoundedJsonSource<T>(
|
|
|
493
497
|
const length = parseContentLength(declared)
|
|
494
498
|
if (length === undefined)
|
|
495
499
|
return onResult === undefined
|
|
496
|
-
? Promise.resolve(
|
|
497
|
-
: Promise.resolve(
|
|
500
|
+
? Promise.resolve(plainError(400, "invalid_content_length"))
|
|
501
|
+
: Promise.resolve(plainError(400, "invalid_content_length")).then(onResult)
|
|
498
502
|
if (length > maxBytes)
|
|
499
503
|
return onResult === undefined
|
|
500
|
-
? Promise.resolve(
|
|
501
|
-
: Promise.resolve(
|
|
504
|
+
? Promise.resolve(plainError(413, "payload_too_large"))
|
|
505
|
+
: Promise.resolve(plainError(413, "payload_too_large")).then(onResult)
|
|
506
|
+
// Trusted framing (Bun's native route table, Deno.serve): the runtime's HTTP parser already
|
|
507
|
+
// delimited the bytes, so the declared length IS the transport frame and chunked routing is
|
|
508
|
+
// moot - the over-cap 413 above already enforced the byte cap. Take the runtime's fused
|
|
509
|
+
// decode+parse straight away, before the transfer-encoding probe that only exists to route
|
|
510
|
+
// *untrusted* Node/adapter sources to the streaming lane. Skipping that probe drops one
|
|
511
|
+
// `Headers.get()` from every Bun/Deno JSON POST; Node/adapter sources are never marked and fall
|
|
512
|
+
// through below. The parsed-value poisoning walk still applies.
|
|
513
|
+
if (hasTrustedBodyFraming(req)) {
|
|
514
|
+
return raw.json().then(
|
|
515
|
+
(parsed) => {
|
|
516
|
+
let guarded: unknown
|
|
517
|
+
try {
|
|
518
|
+
guarded = guardParsedValue(parsed, protoPoisoning)
|
|
519
|
+
} catch {
|
|
520
|
+
return onResult === undefined
|
|
521
|
+
? plainError(400, "invalid_json")
|
|
522
|
+
: onResult(plainError(400, "invalid_json"))
|
|
523
|
+
}
|
|
524
|
+
return onResult === undefined ? guarded : onResult(guarded)
|
|
525
|
+
},
|
|
526
|
+
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
527
|
+
)
|
|
528
|
+
}
|
|
502
529
|
const chunked =
|
|
503
530
|
(sourceHeader === undefined
|
|
504
531
|
? req.headers.get("transfer-encoding")
|
|
505
532
|
: sourceHeader.call(req, "transfer-encoding")) !== null
|
|
506
533
|
if (!chunked) {
|
|
507
|
-
// Bun's compiled native route table receives a Request whose bytes were already delimited by
|
|
508
|
-
// Bun's HTTP parser. Keep the exact-byte path for every portable/adapted source, but preserve
|
|
509
|
-
// Bun's fused decode+parse on this trusted ingress; the declared length is the transport frame
|
|
510
|
-
// there, not an attacker-controlled hint. The parsed-value poisoning walk still applies.
|
|
511
|
-
if (hasTrustedBodyFraming(req)) {
|
|
512
|
-
return raw.json().then(
|
|
513
|
-
(parsed) => {
|
|
514
|
-
let guarded: unknown
|
|
515
|
-
try {
|
|
516
|
-
guarded = guardParsedValue(parsed, protoPoisoning)
|
|
517
|
-
} catch {
|
|
518
|
-
return onResult === undefined
|
|
519
|
-
? jsonError(400, "invalid_json")
|
|
520
|
-
: onResult(jsonError(400, "invalid_json"))
|
|
521
|
-
}
|
|
522
|
-
return onResult === undefined ? guarded : onResult(guarded)
|
|
523
|
-
},
|
|
524
|
-
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
525
|
-
)
|
|
526
|
-
}
|
|
527
534
|
const jsonWithByteLength = raw.jsonWithByteLength
|
|
528
535
|
if (jsonWithByteLength !== undefined) {
|
|
529
536
|
return jsonWithByteLength.call(raw).then(
|
|
530
537
|
({ value, byteLength }) => {
|
|
531
538
|
if (byteLength > length || byteLength > maxBytes) {
|
|
532
539
|
return onResult === undefined
|
|
533
|
-
?
|
|
534
|
-
: onResult(
|
|
540
|
+
? plainError(413, "payload_too_large")
|
|
541
|
+
: onResult(plainError(413, "payload_too_large"))
|
|
535
542
|
}
|
|
536
543
|
let guarded: unknown
|
|
537
544
|
try {
|
|
538
545
|
guarded = guardParsedValue(value, protoPoisoning)
|
|
539
546
|
} catch {
|
|
540
547
|
return onResult === undefined
|
|
541
|
-
?
|
|
542
|
-
: onResult(
|
|
548
|
+
? plainError(400, "invalid_json")
|
|
549
|
+
: onResult(plainError(400, "invalid_json"))
|
|
543
550
|
}
|
|
544
551
|
return onResult === undefined ? guarded : onResult(guarded)
|
|
545
552
|
},
|
|
@@ -552,16 +559,16 @@ export function readBoundedJsonSource<T>(
|
|
|
552
559
|
(bytes) => {
|
|
553
560
|
if (bytes.byteLength > length || bytes.byteLength > maxBytes) {
|
|
554
561
|
return onResult === undefined
|
|
555
|
-
?
|
|
556
|
-
: onResult(
|
|
562
|
+
? plainError(413, "payload_too_large")
|
|
563
|
+
: onResult(plainError(413, "payload_too_large"))
|
|
557
564
|
}
|
|
558
565
|
try {
|
|
559
566
|
const parsed = parseJsonGuarded(TEXT_DECODER.decode(bytes), protoPoisoning)
|
|
560
567
|
return onResult === undefined ? parsed : onResult(parsed)
|
|
561
568
|
} catch {
|
|
562
569
|
return onResult === undefined
|
|
563
|
-
?
|
|
564
|
-
: onResult(
|
|
570
|
+
? plainError(400, "invalid_json")
|
|
571
|
+
: onResult(plainError(400, "invalid_json"))
|
|
565
572
|
}
|
|
566
573
|
},
|
|
567
574
|
() => (onResult === undefined ? INVALID_JSON() : onResult(INVALID_JSON())),
|
|
@@ -578,16 +585,16 @@ export function readBoundedJsonSource<T>(
|
|
|
578
585
|
const bytes = new Uint8Array(buffer)
|
|
579
586
|
if (bytes.byteLength > length || bytes.byteLength > maxBytes) {
|
|
580
587
|
return onResult === undefined
|
|
581
|
-
?
|
|
582
|
-
: onResult(
|
|
588
|
+
? plainError(413, "payload_too_large")
|
|
589
|
+
: onResult(plainError(413, "payload_too_large"))
|
|
583
590
|
}
|
|
584
591
|
let parsed: unknown
|
|
585
592
|
try {
|
|
586
593
|
parsed = parseJsonGuarded(TEXT_DECODER.decode(bytes), protoPoisoning)
|
|
587
594
|
} catch {
|
|
588
595
|
return onResult === undefined
|
|
589
|
-
?
|
|
590
|
-
: onResult(
|
|
596
|
+
? plainError(400, "invalid_json")
|
|
597
|
+
: onResult(plainError(400, "invalid_json"))
|
|
591
598
|
}
|
|
592
599
|
return onResult === undefined ? parsed : onResult(parsed)
|
|
593
600
|
},
|
|
@@ -602,21 +609,69 @@ export function readBoundedJsonSource<T>(
|
|
|
602
609
|
/** The shared rejection handler for a body read that never completed (client abort, socket error,
|
|
603
610
|
* malformed framing): the same flat 400 a syntactically invalid body gets. Hoisted so the framed
|
|
604
611
|
* lane allocates no per-request closure for it. */
|
|
605
|
-
const INVALID_JSON = ():
|
|
612
|
+
const INVALID_JSON = (): ResponseResult => plainError(400, "invalid_json")
|
|
606
613
|
|
|
607
614
|
/** The chunked / length-less fallback: drain under the streaming byte cap, then parse guarded. */
|
|
608
615
|
async function readStreamedJsonSource(
|
|
609
616
|
raw: RequestSource,
|
|
610
617
|
maxBytes: number,
|
|
611
618
|
protoPoisoning: ProtoPoisoning,
|
|
612
|
-
): Promise<unknown |
|
|
619
|
+
): Promise<unknown | ResponseResult> {
|
|
613
620
|
const body = raw.body
|
|
614
|
-
if (body === null) return
|
|
621
|
+
if (body === null) return plainError(400, "invalid_json")
|
|
615
622
|
const drained = await drainCapped(body, maxBytes)
|
|
616
|
-
if (!drained.ok) return
|
|
623
|
+
if (!drained.ok) return plainError(413, "payload_too_large")
|
|
617
624
|
try {
|
|
618
625
|
return parseJsonGuarded(TEXT_DECODER.decode(drained.bytes), protoPoisoning)
|
|
619
626
|
} catch {
|
|
620
|
-
return
|
|
627
|
+
return plainError(400, "invalid_json")
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* The shared bounded body framing/parser: the single trust-boundary enforcement point every body
|
|
633
|
+
* lane routes through. Content-type dispatch, the urlencoded-form cap, and the JSON path
|
|
634
|
+
* (`Content-Length` pre-reject -> streaming cap -> prototype-poisoning guard, all inside
|
|
635
|
+
* `readBoundedJsonSource`) live here so the fused body runner, the generic body-only lane, and a
|
|
636
|
+
* compact edge entrypoint all enforce the same boundary from one definition. `protoPoisoning` is a
|
|
637
|
+
* parameter, not read off a server instance, so this stays free of any dependency on the class.
|
|
638
|
+
*
|
|
639
|
+
* `onParsed` receives the decoded, guard-screened value; a framing rejection (`Content-Length` over
|
|
640
|
+
* the cap, a lying length caught mid-stream, malformed JSON, an unsupported media type) is rendered
|
|
641
|
+
* through `wrapResponse`; a read that never completed (client abort, socket error) reaches `onError`.
|
|
642
|
+
*/
|
|
643
|
+
export function readBodyFramed<T>(
|
|
644
|
+
source: RequestSource,
|
|
645
|
+
maxBodyBytes: number,
|
|
646
|
+
protoPoisoning: ProtoPoisoning,
|
|
647
|
+
onParsed: (parsed: unknown) => MaybePromise<T>,
|
|
648
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
649
|
+
onError: (err: unknown) => MaybePromise<T>,
|
|
650
|
+
): Promise<T> {
|
|
651
|
+
const contentType = headerOf(source, "content-type") ?? ""
|
|
652
|
+
if (contentType !== "application/json" && !contentType.includes("application/json")) {
|
|
653
|
+
if (isUrlEncodedForm(contentType)) {
|
|
654
|
+
return readBoundedForm(source, maxBodyBytes).then(
|
|
655
|
+
(form) => (isResponseResult(form) ? wrapResponse(form) : onParsed(form)),
|
|
656
|
+
onError,
|
|
657
|
+
) as Promise<T>
|
|
658
|
+
}
|
|
659
|
+
// multipart/form-data (file uploads) stays 415 on the schema path by design - files don't fit a
|
|
660
|
+
// value schema; use a schema-less route + @nifrajs/uploads helpers for those.
|
|
661
|
+
return Promise.resolve(wrapResponse(plainError(415, "unsupported_media_type")))
|
|
662
|
+
}
|
|
663
|
+
// All JSON bodies go through readBoundedJsonSource - the single enforcement point for the framed
|
|
664
|
+
// fast path (post-read byte re-check), the streaming cap, and the prototype-poisoning guard. A
|
|
665
|
+
// separate inlined native-json() fast path here would fork the trust boundary in two.
|
|
666
|
+
try {
|
|
667
|
+
return readBoundedJsonSource(
|
|
668
|
+
source,
|
|
669
|
+
maxBodyBytes,
|
|
670
|
+
protoPoisoning,
|
|
671
|
+
(parsed) => (isResponseResult(parsed) ? wrapResponse(parsed) : onParsed(parsed)),
|
|
672
|
+
onError,
|
|
673
|
+
)
|
|
674
|
+
} catch (err) {
|
|
675
|
+
return Promise.resolve(onError(err))
|
|
621
676
|
}
|
|
622
677
|
}
|
package/src/server/respond.ts
CHANGED
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* spine types, so the wire-response format lives in one module distinct from the request engine.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { NODE_BRIDGE_MARKER_KEYS } from "./bridge-markers.ts"
|
|
7
8
|
import {
|
|
8
9
|
CONTEXT_SET,
|
|
9
10
|
EMPTY_RESPONSE_CONTROLS,
|
|
10
11
|
type HandlerResult,
|
|
11
12
|
isResponseResult,
|
|
13
|
+
type PlainRender,
|
|
14
|
+
plainRenderHeaders,
|
|
12
15
|
} from "./runtime-core.ts"
|
|
13
16
|
import type { CtxSet, RawContext } from "./server.ts"
|
|
14
17
|
import { mergeStaticHeaderRecord, type StaticResponseHeaders } from "./static-headers.ts"
|
|
@@ -24,6 +27,16 @@ function headersInit(set: CtxSet): Record<string, string> | Headers | undefined
|
|
|
24
27
|
return headers
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
/** The `c.set` a plain render stands for: its status and headers, carrying over the request's queued
|
|
31
|
+
* cookies so `set.cookie(...)` then an early exit still ships them. */
|
|
32
|
+
function plainRenderSet(plain: PlainRender, set: CtxSet): CtxSet {
|
|
33
|
+
return {
|
|
34
|
+
status: plain.status,
|
|
35
|
+
_headers: plainRenderHeaders(plain, set),
|
|
36
|
+
_cookies: set._cookies,
|
|
37
|
+
} as CtxSet
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
// Keep the fast JSON respond path byte-identical to `Response.json` without probing it at module
|
|
28
41
|
// scope: workerd forbids `Response.json()` during startup. A shared `Headers` is safe to reuse
|
|
29
42
|
// across responses: the Response constructor copies `init.headers` into its own list.
|
|
@@ -178,7 +191,7 @@ function denoResponseWithJsonBody(
|
|
|
178
191
|
// The framework-buffered-body marker shared with the Node adapter: a Response carrying it exposes
|
|
179
192
|
// its already-serialized bytes without draining. On the Web serving paths it exists ONLY when a
|
|
180
193
|
// registered body/raw hook needs it, so hook-less apps pay nothing.
|
|
181
|
-
const RESPONSE_BODY = Symbol.for(
|
|
194
|
+
const RESPONSE_BODY = Symbol.for(NODE_BRIDGE_MARKER_KEYS.responseBody)
|
|
182
195
|
|
|
183
196
|
/**
|
|
184
197
|
* Headers objects the framework itself constructed - guaranteed mutable, no guard. Response
|
|
@@ -396,6 +409,18 @@ export function toResponse(
|
|
|
396
409
|
statics?: StaticResponseHeaders,
|
|
397
410
|
): Response {
|
|
398
411
|
if (isResponseResult(result)) {
|
|
412
|
+
const plain = result.plain
|
|
413
|
+
if (plain !== undefined) {
|
|
414
|
+
// Rendered as the value it is, on the branches below, rather than through `toResponse()`: the
|
|
415
|
+
// status and headers fold into a `set` and the body takes the ordinary JSON lane. One level
|
|
416
|
+
// only - a plain render's body is a user value, never another response carrier.
|
|
417
|
+
return toResponse(
|
|
418
|
+
plain.body as HandlerResult,
|
|
419
|
+
plainRenderSet(plain, set),
|
|
420
|
+
tagResponseBody,
|
|
421
|
+
statics,
|
|
422
|
+
)
|
|
423
|
+
}
|
|
399
424
|
return withStatics(
|
|
400
425
|
appendCookiesToResponse(normalizeBodylessResponse(result.toResponse()), set),
|
|
401
426
|
statics,
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
} from "../schema/standard.ts"
|
|
49
49
|
import { INSTALL_RESPONSE_CONTRACT } from "./install.ts"
|
|
50
50
|
import type { IdentityPlugin } from "./plugin.ts"
|
|
51
|
+
import { isResponseResult } from "./runtime-core.ts"
|
|
51
52
|
import type { AnyServer } from "./server.ts"
|
|
52
53
|
|
|
53
54
|
/**
|
|
@@ -91,8 +92,11 @@ export function checkResponseContract(
|
|
|
91
92
|
mode: "warn" | "enforce",
|
|
92
93
|
): ResponseContractOutcome | Promise<ResponseContractOutcome> {
|
|
93
94
|
// A handler may return a raw Response as deliberate control flow (a redirect, a stream). There is no
|
|
94
|
-
// JSON payload to hold to the contract, and re-serializing one would corrupt it.
|
|
95
|
-
|
|
95
|
+
// JSON payload to hold to the contract, and re-serializing one would corrupt it. A `status(...)` is
|
|
96
|
+
// the same control flow in plain-data form: the contract describes the route's SUCCESS payload, so
|
|
97
|
+
// holding an early exit (a 401 body, say) to it would fail every guarded route that declares one.
|
|
98
|
+
if (result instanceof Response || result === undefined || isResponseResult(result))
|
|
99
|
+
return { kind: "ok", value: result }
|
|
96
100
|
const settled = schema["~standard"].validate(result)
|
|
97
101
|
return settled instanceof Promise
|
|
98
102
|
? settled.then((r) => interpret(r, result, mode))
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NodeRequestContext,
|
|
3
|
+
NodeResponseHook,
|
|
4
|
+
ResponseBodyHook,
|
|
5
|
+
ResponseHeadersHook,
|
|
6
|
+
ResponseHeadersView,
|
|
7
|
+
} from "./node-outcome-hook.ts"
|
|
8
|
+
import { recordHeadersView } from "./node-outcome-hook.ts"
|
|
9
|
+
import { knownMutableHeaders, rememberMutableHeaders, taggedResponseBody } from "./respond.ts"
|
|
10
|
+
import { applyBodyReplacement, withReplacedBody } from "./response-hooks.ts"
|
|
11
|
+
|
|
12
|
+
type MaybePromise<T> = T | Promise<T>
|
|
13
|
+
type WebResponseHook = (response: Response, req: Request) => MaybePromise<Response>
|
|
14
|
+
|
|
15
|
+
export type { ResponseBodyHook, ResponseHeadersHook } from "./node-outcome-hook.ts"
|
|
16
|
+
|
|
17
|
+
export interface ResponseObserverHost {
|
|
18
|
+
assertConfigurable(operation: string): void
|
|
19
|
+
addResponseHook(web: WebResponseHook, node: NodeResponseHook | undefined): void
|
|
20
|
+
enableResponseBodyTagging(): object
|
|
21
|
+
responseBodyOwners(): ReadonlySet<object>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ResponseObserverMethods {
|
|
25
|
+
onResponseHeaders(fn: ResponseHeadersHook): this
|
|
26
|
+
onResponseBody(fn: ResponseBodyHook): this
|
|
27
|
+
onResponseRaw(fn: (response: Response, req: Request) => MaybePromise<Response>): this
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ResponseObserverRuntime {
|
|
31
|
+
install(host: ResponseObserverHost): ResponseObserverMethods
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const HEADER_MUTABILITY_PROBE = "x-nifra-header-probe"
|
|
35
|
+
const GUARDED_RESPONSE_HEADERS = new WeakSet<Headers>()
|
|
36
|
+
|
|
37
|
+
function hasMutableResponseHeaders(headers: Headers): boolean {
|
|
38
|
+
if (knownMutableHeaders(headers)) return true
|
|
39
|
+
if (GUARDED_RESPONSE_HEADERS.has(headers)) return false
|
|
40
|
+
let previous: string | null = null
|
|
41
|
+
try {
|
|
42
|
+
previous = headers.get(HEADER_MUTABILITY_PROBE)
|
|
43
|
+
headers.set(HEADER_MUTABILITY_PROBE, "1")
|
|
44
|
+
if (previous === null) headers.delete(HEADER_MUTABILITY_PROBE)
|
|
45
|
+
else headers.set(HEADER_MUTABILITY_PROBE, previous)
|
|
46
|
+
rememberMutableHeaders(headers)
|
|
47
|
+
return true
|
|
48
|
+
} catch {
|
|
49
|
+
try {
|
|
50
|
+
if (previous === null) headers.delete(HEADER_MUTABILITY_PROBE)
|
|
51
|
+
else headers.set(HEADER_MUTABILITY_PROBE, previous)
|
|
52
|
+
} catch {
|
|
53
|
+
// A guarded response rejected the probe and the clone path is authoritative.
|
|
54
|
+
}
|
|
55
|
+
GUARDED_RESPONSE_HEADERS.add(headers)
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function webRequestView(req: Request): NodeRequestContext {
|
|
61
|
+
return { method: req.method, url: req.url, header: (name) => req.headers.get(name) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function webHeadersView(
|
|
65
|
+
response: Response,
|
|
66
|
+
req: Request,
|
|
67
|
+
): {
|
|
68
|
+
readonly response: Response
|
|
69
|
+
readonly headers: ResponseHeadersView
|
|
70
|
+
readonly request: NodeRequestContext
|
|
71
|
+
} {
|
|
72
|
+
const request = webRequestView(req)
|
|
73
|
+
if (!hasMutableResponseHeaders(response.headers)) {
|
|
74
|
+
const clone = new Response(response.body, response)
|
|
75
|
+
return { response: clone, headers: clone.headers, request }
|
|
76
|
+
}
|
|
77
|
+
return { response, headers: response.headers, request }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createResponseObserverRuntime(): ResponseObserverRuntime {
|
|
81
|
+
return {
|
|
82
|
+
install(host) {
|
|
83
|
+
return {
|
|
84
|
+
onResponseHeaders(this: ResponseObserverMethods, fn): ResponseObserverMethods {
|
|
85
|
+
host.assertConfigurable("onResponseHeaders()")
|
|
86
|
+
host.addResponseHook(
|
|
87
|
+
(response, req) => {
|
|
88
|
+
const view = webHeadersView(response, req)
|
|
89
|
+
const out = fn(view.headers, view.request, response.status)
|
|
90
|
+
return out instanceof Promise ? out.then(() => view.response) : view.response
|
|
91
|
+
},
|
|
92
|
+
(response, req) => fn(recordHeadersView(response), req, response.status),
|
|
93
|
+
)
|
|
94
|
+
return this
|
|
95
|
+
},
|
|
96
|
+
onResponseBody(this: ResponseObserverMethods, fn): ResponseObserverMethods {
|
|
97
|
+
host.assertConfigurable("onResponseBody()")
|
|
98
|
+
host.enableResponseBodyTagging()
|
|
99
|
+
host.addResponseHook(
|
|
100
|
+
(response, req) => {
|
|
101
|
+
const body = taggedResponseBody(response, host.responseBodyOwners())
|
|
102
|
+
if (body === undefined) return response
|
|
103
|
+
const view = webHeadersView(response, req)
|
|
104
|
+
const out = fn(body, view.headers, view.request, response.status)
|
|
105
|
+
return out instanceof Promise
|
|
106
|
+
? out.then((replaced) => withReplacedBody(response, replaced))
|
|
107
|
+
: withReplacedBody(response, out)
|
|
108
|
+
},
|
|
109
|
+
(response, req) => {
|
|
110
|
+
if (response.body === null) return undefined
|
|
111
|
+
const out = fn(response.body, recordHeadersView(response), req, response.status)
|
|
112
|
+
if (out instanceof Promise)
|
|
113
|
+
return out.then((replaced) => applyBodyReplacement(response, replaced))
|
|
114
|
+
applyBodyReplacement(response, out)
|
|
115
|
+
return undefined
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
return this
|
|
119
|
+
},
|
|
120
|
+
onResponseRaw(this: ResponseObserverMethods, fn): ResponseObserverMethods {
|
|
121
|
+
host.assertConfigurable("onResponseRaw()")
|
|
122
|
+
host.enableResponseBodyTagging()
|
|
123
|
+
host.addResponseHook(
|
|
124
|
+
(response, req) =>
|
|
125
|
+
taggedResponseBody(response, host.responseBodyOwners()) === undefined
|
|
126
|
+
? fn(response, req)
|
|
127
|
+
: response,
|
|
128
|
+
() => undefined,
|
|
129
|
+
)
|
|
130
|
+
return this
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
}
|