@nifrajs/core 5.5.1 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +246 -0
- package/README.md +5 -0
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js.map +1 -1
- package/dist/binary.d.ts +15 -1
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +1 -0
- package/dist/binary.js.map +1 -1
- package/dist/edge-kit.d.ts +1 -0
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +1 -0
- package/dist/edge-kit.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +4 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +4 -0
- package/dist/evidence.js.map +1 -1
- package/dist/file-type.d.ts +30 -0
- package/dist/file-type.d.ts.map +1 -0
- package/dist/file-type.js +118 -0
- package/dist/file-type.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/hook-origins.d.ts +2 -0
- package/dist/internal/hook-origins.d.ts.map +1 -1
- package/dist/internal/hook-origins.js +12 -0
- package/dist/internal/hook-origins.js.map +1 -1
- package/dist/internal/route-program.d.ts +11 -0
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +133 -85
- package/dist/internal/route-program.js.map +1 -1
- package/dist/reflection.d.ts +19 -1
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +46 -7
- package/dist/reflection.js.map +1 -1
- package/dist/response-observer.d.ts +1 -0
- package/dist/response-observer.d.ts.map +1 -1
- package/dist/response-observer.js +1 -0
- package/dist/response-observer.js.map +1 -1
- package/dist/server/answer-step.d.ts +27 -0
- package/dist/server/answer-step.d.ts.map +1 -0
- package/dist/server/answer-step.js +33 -0
- package/dist/server/answer-step.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +4 -1
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +9 -3
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/base64-answers.d.ts +5 -0
- package/dist/server/base64-answers.d.ts.map +1 -0
- package/dist/server/base64-answers.js +132 -0
- package/dist/server/base64-answers.js.map +1 -0
- package/dist/server/before-handler.d.ts +10 -0
- package/dist/server/before-handler.d.ts.map +1 -0
- package/dist/server/before-handler.js +12 -0
- package/dist/server/before-handler.js.map +1 -0
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +8 -2
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body-parser.d.ts.map +1 -1
- package/dist/server/body-parser.js +2 -5
- package/dist/server/body-parser.js.map +1 -1
- package/dist/server/body.d.ts +24 -8
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +86 -33
- package/dist/server/body.js.map +1 -1
- package/dist/server/core-copies.d.ts.map +1 -1
- package/dist/server/core-copies.js +5 -2
- package/dist/server/core-copies.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +6 -17
- package/dist/server/http.js.map +1 -1
- package/dist/server/install.d.ts +2 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +2 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/json-response.d.ts +3 -0
- package/dist/server/json-response.d.ts.map +1 -0
- package/dist/server/json-response.js +14 -0
- package/dist/server/json-response.js.map +1 -0
- package/dist/server/json-text.d.ts +16 -0
- package/dist/server/json-text.d.ts.map +1 -0
- package/dist/server/json-text.js +34 -0
- package/dist/server/json-text.js.map +1 -0
- package/dist/server/multipart.d.ts +3 -0
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/multipart.js +226 -97
- package/dist/server/multipart.js.map +1 -1
- package/dist/server/node-outcome.d.ts +5 -0
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +3 -4
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/proto-guard.d.ts.map +1 -1
- package/dist/server/proto-guard.js +39 -4
- package/dist/server/proto-guard.js.map +1 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +8 -14
- package/dist/server/query.js.map +1 -1
- package/dist/server/repeated-prefix.d.ts +17 -0
- package/dist/server/repeated-prefix.d.ts.map +1 -0
- package/dist/server/repeated-prefix.js +17 -0
- package/dist/server/repeated-prefix.js.map +1 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +5 -5
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +3 -4
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +45 -37
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +12 -2
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +137 -9
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/route-template.d.ts +10 -0
- package/dist/server/route-template.d.ts.map +1 -0
- package/dist/server/route-template.js +21 -0
- package/dist/server/route-template.js.map +1 -0
- package/dist/server/runtime-core.d.ts +7 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +10 -2
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +17 -8
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +27 -12
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +326 -108
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +1 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +7 -5
- package/dist/server/sse.js.map +1 -1
- package/dist/server/text-slots.d.ts +8 -0
- package/dist/server/text-slots.d.ts.map +1 -0
- package/dist/server/text-slots.js +19 -0
- package/dist/server/text-slots.js.map +1 -0
- package/dist/server/webhook.d.ts +1 -1
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +24 -19
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +18 -3
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +45 -5
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +7 -4
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts +5 -0
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +26 -17
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-cli.d.ts +64 -0
- package/dist/tool-cli.d.ts.map +1 -0
- package/dist/tool-cli.js +495 -0
- package/dist/tool-cli.js.map +1 -0
- package/dist/transport-codec.d.ts.map +1 -1
- package/dist/transport-codec.js +2 -1
- package/dist/transport-codec.js.map +1 -1
- package/package.json +11 -1
- package/src/assurance.ts +1 -0
- package/src/binary.ts +17 -0
- package/src/edge-kit.ts +1 -0
- package/src/errors.ts +1 -0
- package/src/evidence.ts +8 -0
- package/src/file-type.ts +130 -0
- package/src/index.ts +3 -1
- package/src/internal/hook-origins.ts +10 -0
- package/src/internal/route-program.ts +261 -93
- package/src/reflection.ts +71 -7
- package/src/response-observer.ts +6 -0
- package/src/server/answer-step.ts +68 -0
- package/src/server/bare-error-lane.ts +9 -4
- package/src/server/base64-answers.ts +138 -0
- package/src/server/before-handler.ts +14 -0
- package/src/server/body-lane.ts +19 -9
- package/src/server/body-parser.ts +1 -5
- package/src/server/body.ts +96 -36
- package/src/server/core-copies.ts +5 -2
- package/src/server/http.ts +6 -15
- package/src/server/install.ts +3 -0
- package/src/server/json-response.ts +13 -0
- package/src/server/json-text.ts +34 -0
- package/src/server/multipart.ts +232 -100
- package/src/server/node-outcome.ts +4 -5
- package/src/server/proto-guard.ts +39 -4
- package/src/server/query.ts +7 -13
- package/src/server/repeated-prefix.ts +25 -0
- package/src/server/request-context.ts +5 -5
- package/src/server/respond.ts +47 -30
- package/src/server/response-contract-lane.ts +132 -9
- package/src/server/route-template.ts +24 -0
- package/src/server/runtime-core.ts +12 -2
- package/src/server/server-types.ts +18 -6
- package/src/server/server.ts +361 -112
- package/src/server/sse.ts +8 -6
- package/src/server/text-slots.ts +21 -0
- package/src/server/webhook.ts +29 -24
- package/src/server/websocket.ts +57 -8
- package/src/server/ws-bun.ts +7 -3
- package/src/server.ts +2 -0
- package/src/single-copy.ts +31 -15
- package/src/tool-cli.ts +584 -0
- package/src/transport-codec.ts +3 -1
package/src/server/install.ts
CHANGED
|
@@ -52,5 +52,8 @@ export const RESOLVE_NODE_MOUNT: unique symbol = Symbol.for(
|
|
|
52
52
|
NODE_BRIDGE_MARKER_KEYS.resolveNodeMount,
|
|
53
53
|
)
|
|
54
54
|
|
|
55
|
+
/** @internal Run a server's `onStopping` or `onStop` hooks; see `Server.stop()`. */
|
|
56
|
+
export const SERVER_LIFECYCLE: unique symbol = Symbol.for("@nifrajs/core/server-lifecycle")
|
|
57
|
+
|
|
55
58
|
/** @internal Marker on a mounted handler that knows how to serve a native Node request/response. */
|
|
56
59
|
export const NODE_NATIVE_MOUNT: unique symbol = Symbol.for("@nifrajs/core/node-native-mount")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { retried } from "./json-text.ts"
|
|
2
|
+
|
|
3
|
+
/** `Response.json`, with each `bigint` written as its decimal string, as `jsonText` writes it. */
|
|
4
|
+
export function jsonResponse(value: unknown, init?: ResponseInit): Response {
|
|
5
|
+
try {
|
|
6
|
+
return Response.json(value, init)
|
|
7
|
+
} catch (error) {
|
|
8
|
+
const text = retried(value, error)
|
|
9
|
+
// Parsed back so the runtime's own `Response.json` still picks the content-type, and still
|
|
10
|
+
// throws for a value with no JSON form.
|
|
11
|
+
return Response.json(typeof text === "string" ? JSON.parse(text) : value, init)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON has no `bigint`, and `JSON.stringify` throws on one, so a handler returning a database's 64-bit
|
|
3
|
+
* id answered a 500. Every plain JSON writer goes through this instead: a `bigint` is written as its
|
|
4
|
+
* decimal string, the encoding OpenAPI generators and protobuf's JSON mapping use for 64-bit integers.
|
|
5
|
+
* Only a first attempt that threw is retried, so a value without one costs nothing more. The Node
|
|
6
|
+
* adapter runs a generated copy (scripts/gen-node-outcome.ts), so this module imports nothing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const bigintAsText = (_key: string, value: unknown): unknown =>
|
|
10
|
+
typeof value === "bigint" ? value.toString() : value
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `JSON.stringify`, with each `bigint` written as its decimal string. Typed as the lib types
|
|
14
|
+
* `JSON.stringify`: a value with no JSON form (`undefined`, a function) still gives `undefined`.
|
|
15
|
+
*/
|
|
16
|
+
export function jsonText(value: unknown): string {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(value)
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return retried(value, error)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Write `value` again, each `bigint` as text, after `JSON.stringify(value)` threw `error`; any other
|
|
25
|
+
* failure rethrows `error`. */
|
|
26
|
+
export function retried(value: unknown, error: unknown): string {
|
|
27
|
+
if (!(error instanceof TypeError)) throw error
|
|
28
|
+
try {
|
|
29
|
+
return JSON.stringify(value, bigintAsText)
|
|
30
|
+
} catch {
|
|
31
|
+
// A cycle or a throwing `toJSON` fails this pass too; the first error is the one to report.
|
|
32
|
+
throw error
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/server/multipart.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* A route reads JSON and urlencoded bodies by default and answers `415` to everything else. Wrapping
|
|
5
5
|
* its body schema opts that one route into multipart: the parts are read under the route's
|
|
6
6
|
* `bodyLimit`, bounded by count and size, collected into one record, and handed to the schema.
|
|
7
|
+
* The body is parsed here, by the Fetch standard's `multipart/form-data` parser, and not by the
|
|
8
|
+
* runtime's own `formData()`: those disagree on bare line feeds, parts with no name, escaped or
|
|
9
|
+
* percent-encoded names, and a file's default type, so one body was a different form on each runtime.
|
|
7
10
|
*
|
|
8
11
|
* import { multipartBody } from "@nifrajs/core/multipart"
|
|
9
12
|
* import { t } from "@nifrajs/schema"
|
|
@@ -73,6 +76,8 @@ const MAX_FILE_NAME_LENGTH = 255
|
|
|
73
76
|
const DASH = 45
|
|
74
77
|
const CR = 13
|
|
75
78
|
const LF = 10
|
|
79
|
+
const COLON = 58
|
|
80
|
+
const PERCENT = 37
|
|
76
81
|
const QUOTE = 34
|
|
77
82
|
const BACKSLASH = 92
|
|
78
83
|
const SLASH = 47
|
|
@@ -82,6 +87,23 @@ const TAB = 9
|
|
|
82
87
|
|
|
83
88
|
const INVALID = (): ResponseResult => plainError(400, "invalid_multipart")
|
|
84
89
|
|
|
90
|
+
const ASCII = new TextEncoder()
|
|
91
|
+
// The Fetch standard reads a disposition only in this exact form, which every browser writes.
|
|
92
|
+
const FORM_DATA_NAME = ASCII.encode('form-data; name="')
|
|
93
|
+
const FILE_NAME = ASCII.encode('; filename="')
|
|
94
|
+
const CONTENT_DISPOSITION = ASCII.encode("content-disposition")
|
|
95
|
+
const CONTENT_TYPE = ASCII.encode("content-type")
|
|
96
|
+
// "UTF-8 decode without BOM": a leading BOM is kept as U+FEFF, not dropped.
|
|
97
|
+
const UTF8 = new TextDecoder("utf-8", { ignoreBOM: true })
|
|
98
|
+
// A short ASCII name or value is read code by code: a decoder call costs more than the bytes.
|
|
99
|
+
const SHORT_TEXT = 64
|
|
100
|
+
|
|
101
|
+
// RFC 9110 `tchar`, by byte value.
|
|
102
|
+
const TCHAR = new Uint8Array(128)
|
|
103
|
+
for (const char of "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") {
|
|
104
|
+
TCHAR[char.charCodeAt(0)] = 1
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
function limitOf(value: number | undefined, fallback: number, name: string): number {
|
|
86
108
|
if (value === undefined) return fallback
|
|
87
109
|
assertByteLimit(value, `multipartBody: ${name}`)
|
|
@@ -150,14 +172,10 @@ function failureTable(pattern: Uint8Array): Uint8Array {
|
|
|
150
172
|
return table
|
|
151
173
|
}
|
|
152
174
|
|
|
153
|
-
/**
|
|
154
|
-
* What a scan for `--boundary` found: how many, how many of them close a part with no content, and
|
|
155
|
-
* whether the last one closes the body.
|
|
156
|
-
*/
|
|
175
|
+
/** What a scan for `--boundary` found: where each one starts, and how many close an empty part. */
|
|
157
176
|
interface Delimiters {
|
|
158
|
-
readonly
|
|
177
|
+
readonly at: readonly number[]
|
|
159
178
|
readonly empty: number
|
|
160
|
-
readonly closed: boolean
|
|
161
179
|
}
|
|
162
180
|
|
|
163
181
|
/** The most delimiters a scan counts: past either budget the body is refused unparsed. */
|
|
@@ -166,11 +184,6 @@ interface DelimiterLimits {
|
|
|
166
184
|
readonly empty: number
|
|
167
185
|
}
|
|
168
186
|
|
|
169
|
-
/** `end` is the index after the last delimiter, or -1 when there was none. */
|
|
170
|
-
function delimiters(bytes: Uint8Array, count: number, empty: number, end: number): Delimiters {
|
|
171
|
-
return { count, empty, closed: end !== -1 && bytes[end] === DASH && bytes[end + 1] === DASH }
|
|
172
|
-
}
|
|
173
|
-
|
|
174
187
|
/**
|
|
175
188
|
* Whether the delimiter at `at` closes a part with no content: the blank line ending the part's
|
|
176
189
|
* headers is followed at once by the CRLF that opens the delimiter. A part whose own content ends
|
|
@@ -196,30 +209,34 @@ function scanFrom(
|
|
|
196
209
|
bytes: Uint8Array,
|
|
197
210
|
pattern: Uint8Array,
|
|
198
211
|
start: number,
|
|
199
|
-
|
|
212
|
+
at: number[],
|
|
200
213
|
countedEmpty: number,
|
|
201
|
-
lastEnd: number,
|
|
202
214
|
limits: DelimiterLimits,
|
|
203
215
|
): Delimiters {
|
|
204
216
|
const table = failureTable(pattern)
|
|
205
217
|
const size = pattern.length
|
|
206
|
-
let count = counted
|
|
207
218
|
let empty = countedEmpty
|
|
208
|
-
let end = lastEnd
|
|
209
219
|
for (let i = start, k = 0; i < bytes.length; i++) {
|
|
210
220
|
const byte = bytes[i]
|
|
211
221
|
while (k > 0 && byte !== pattern[k]) k = table[k - 1]!
|
|
212
222
|
if (byte === pattern[k]) k++
|
|
213
223
|
if (k === size) {
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
if (overLimits(
|
|
224
|
+
if (at.length > 0 && closesEmptyPart(bytes, i + 1 - size)) empty++
|
|
225
|
+
at.push(i + 1 - size)
|
|
226
|
+
if (overLimits(at.length, empty, limits)) break
|
|
217
227
|
k = 0
|
|
218
228
|
}
|
|
219
229
|
}
|
|
220
|
-
return
|
|
230
|
+
return { at, empty }
|
|
221
231
|
}
|
|
222
232
|
|
|
233
|
+
declare const Deno: unknown
|
|
234
|
+
|
|
235
|
+
// V8's TypedArray `indexOf` reads one element at a time (~0.6 ms a MiB on Node 26), where Buffer's is
|
|
236
|
+
// native on Node and Bun. Deno's Buffer is a slower polyfill, and Workers have none by default.
|
|
237
|
+
const bufferIndexOf =
|
|
238
|
+
typeof Deno === "undefined" && typeof Buffer === "function" ? Buffer.prototype.indexOf : undefined
|
|
239
|
+
|
|
223
240
|
/**
|
|
224
241
|
* Find the non-overlapping occurrences of `pattern` (which starts with a dash), stopping once the
|
|
225
242
|
* count passes `limit`. Every `--boundary` is counted wherever it sits, which is at least what any
|
|
@@ -236,51 +253,199 @@ function scanDelimiters(
|
|
|
236
253
|
): Delimiters {
|
|
237
254
|
const size = pattern.length
|
|
238
255
|
const last = bytes.length - size
|
|
239
|
-
|
|
256
|
+
const at: number[] = []
|
|
240
257
|
let empty = 0
|
|
241
|
-
let end = -1
|
|
242
258
|
let work = 0
|
|
243
259
|
let i = 0
|
|
244
260
|
while (i <= last) {
|
|
245
|
-
i = bytes.indexOf(DASH, i)
|
|
261
|
+
i = bufferIndexOf === undefined ? bytes.indexOf(DASH, i) : bufferIndexOf.call(bytes, DASH, i)
|
|
246
262
|
if (i === -1 || i > last) break
|
|
247
263
|
let k = 1
|
|
248
264
|
while (k < size && bytes[i + k] === pattern[k]) k++
|
|
249
265
|
if (k === size) {
|
|
250
|
-
if (
|
|
266
|
+
if (at.length > 0 && closesEmptyPart(bytes, i)) empty++
|
|
267
|
+
at.push(i)
|
|
251
268
|
i += size
|
|
252
|
-
|
|
253
|
-
if (overLimits(++count, empty, limits)) break
|
|
269
|
+
if (overLimits(at.length, empty, limits)) break
|
|
254
270
|
continue
|
|
255
271
|
}
|
|
256
272
|
work += k
|
|
257
|
-
if (work > bytes.length) return scanFrom(bytes, pattern, i,
|
|
273
|
+
if (work > bytes.length) return scanFrom(bytes, pattern, i, at, empty, limits)
|
|
274
|
+
i++
|
|
275
|
+
}
|
|
276
|
+
return { at, empty }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// What the File constructor keeps of a type: printable ASCII, lower-cased; anything else is "".
|
|
280
|
+
const PRINTABLE = /^[\x20-\x7e]*$/
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* `content` as a File whose type is what the standard makes of `type`. Bun's constructor appends
|
|
284
|
+
* `;charset=utf-8` to an exact lower-case `text/plain`, `text/html` or `application/json`, but not
|
|
285
|
+
* to the same type in upper case, which it lower-cases like every runtime.
|
|
286
|
+
*/
|
|
287
|
+
function fileOf(content: Uint8Array<ArrayBuffer>, name: string, type: string): File {
|
|
288
|
+
const wanted = PRINTABLE.test(type) ? type.toLowerCase() : ""
|
|
289
|
+
const file = new File([content], name, { type: wanted })
|
|
290
|
+
return file.type === wanted ? file : new File([content], name, { type: wanted.toUpperCase() })
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** One part as the parser read it: its field, its file name if it is a file, and its content. */
|
|
294
|
+
interface Part {
|
|
295
|
+
readonly name: string
|
|
296
|
+
readonly filename: string | null
|
|
297
|
+
readonly type: string | null
|
|
298
|
+
readonly start: number
|
|
299
|
+
readonly end: number
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The first index from `from` holding byte `a`, `b` or `c`, or the end of `bytes`. */
|
|
303
|
+
function scanUntil(bytes: Uint8Array, from: number, a: number, b: number, c: number): number {
|
|
304
|
+
let i = from
|
|
305
|
+
while (i < bytes.length) {
|
|
306
|
+
const byte = bytes[i]
|
|
307
|
+
if (byte === a || byte === b || byte === c) break
|
|
258
308
|
i++
|
|
259
309
|
}
|
|
260
|
-
return
|
|
310
|
+
return i
|
|
261
311
|
}
|
|
262
312
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
313
|
+
function startsWith(bytes: Uint8Array, at: number, prefix: Uint8Array): boolean {
|
|
314
|
+
if (at + prefix.length > bytes.length) return false
|
|
315
|
+
for (let i = 0; i < prefix.length; i++) if (bytes[at + i] !== prefix[i]) return false
|
|
316
|
+
return true
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** `bytes[start, end)` as UTF-8 text. */
|
|
320
|
+
function textOf(bytes: Uint8Array, start: number, end: number): string {
|
|
321
|
+
if (end - start <= SHORT_TEXT) {
|
|
322
|
+
let out = ""
|
|
323
|
+
for (let i = start; i < end; i++) {
|
|
324
|
+
const byte = bytes[i]!
|
|
325
|
+
if (byte >= 0x80) return UTF8.decode(bytes.subarray(start, end))
|
|
326
|
+
out += String.fromCharCode(byte)
|
|
327
|
+
}
|
|
328
|
+
return out
|
|
329
|
+
}
|
|
330
|
+
return UTF8.decode(bytes.subarray(start, end))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Whether `bytes[start, end)` spells `lower`, a lower-case ASCII header name, in any case. */
|
|
334
|
+
function spells(bytes: Uint8Array, start: number, end: number, lower: Uint8Array): boolean {
|
|
335
|
+
if (end - start !== lower.length) return false
|
|
336
|
+
// A CR also folds onto `-`, but no header name holds one.
|
|
337
|
+
for (let i = 0; i < lower.length; i++) if ((bytes[start + i]! | 0x20) !== lower[i]) return false
|
|
338
|
+
return true
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Bytes as the code points of the same values ("isomorphic decode"), for a header's value. */
|
|
342
|
+
function isomorphic(bytes: Uint8Array, start: number, end: number): string {
|
|
343
|
+
let out = ""
|
|
344
|
+
for (let i = start; i < end; i += 4096) {
|
|
345
|
+
out += String.fromCharCode(...bytes.subarray(i, Math.min(end, i + 4096)))
|
|
346
|
+
}
|
|
347
|
+
return out
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* A quoted `name` or `filename` from the byte after its opening quote: everything up to the closing
|
|
352
|
+
* one, with `%0A`, `%0D` and `%22` restored. No backslash escapes, as the standard reads it.
|
|
353
|
+
*/
|
|
354
|
+
function quotedName(bytes: Uint8Array, from: number): { value: string; next: number } | undefined {
|
|
355
|
+
const close = scanUntil(bytes, from, LF, CR, QUOTE)
|
|
356
|
+
if (bytes[close] !== QUOTE) return undefined
|
|
357
|
+
let value = textOf(bytes, from, close)
|
|
358
|
+
if (bytes.subarray(from, close).includes(PERCENT)) {
|
|
359
|
+
value = value.replaceAll("%0A", "\n").replaceAll("%0D", "\r").replaceAll("%22", '"')
|
|
360
|
+
}
|
|
361
|
+
return { value, next: close + 1 }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The headers of the part whose first header line starts at `from`, up to the blank line that
|
|
366
|
+
* ends them. Undefined when a line does not parse or the part names no field.
|
|
367
|
+
*/
|
|
368
|
+
function partHeaders(
|
|
369
|
+
bytes: Uint8Array,
|
|
370
|
+
from: number,
|
|
371
|
+
): { name: string; filename: string | null; type: string | null; next: number } | undefined {
|
|
372
|
+
let position = from
|
|
373
|
+
let name: string | null = null
|
|
374
|
+
let filename: string | null = null
|
|
375
|
+
let type: string | null = null
|
|
376
|
+
for (;;) {
|
|
377
|
+
if (bytes[position] === CR && bytes[position + 1] === LF) {
|
|
378
|
+
return name === null ? undefined : { name, filename, type, next: position + 2 }
|
|
379
|
+
}
|
|
380
|
+
const colon = scanUntil(bytes, position, LF, CR, COLON)
|
|
381
|
+
if (bytes[colon] !== COLON) return undefined
|
|
382
|
+
let start = position
|
|
383
|
+
let end = colon
|
|
384
|
+
while (start < end && isBlank(bytes[start]!)) start++
|
|
385
|
+
while (end > start && isBlank(bytes[end - 1]!)) end--
|
|
386
|
+
if (start === end) return undefined
|
|
387
|
+
for (let i = start; i < end; i++) if (TCHAR[bytes[i]!] !== 1) return undefined
|
|
388
|
+
position = colon + 1
|
|
389
|
+
while (isBlank(bytes[position] ?? 0)) position++
|
|
390
|
+
if (spells(bytes, start, end, CONTENT_DISPOSITION)) {
|
|
391
|
+
name = null
|
|
392
|
+
filename = null
|
|
393
|
+
if (!startsWith(bytes, position, FORM_DATA_NAME)) return undefined
|
|
394
|
+
const parsed = quotedName(bytes, position + FORM_DATA_NAME.length)
|
|
395
|
+
if (parsed === undefined) return undefined
|
|
396
|
+
name = parsed.value
|
|
397
|
+
position = parsed.next
|
|
398
|
+
if (startsWith(bytes, position, FILE_NAME)) {
|
|
399
|
+
const file = quotedName(bytes, position + FILE_NAME.length)
|
|
400
|
+
if (file === undefined) return undefined
|
|
401
|
+
filename = file.value
|
|
402
|
+
position = file.next
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
const stop = scanUntil(bytes, position, LF, CR, LF)
|
|
406
|
+
if (spells(bytes, start, end, CONTENT_TYPE)) {
|
|
407
|
+
let end = stop
|
|
408
|
+
while (end > position && isBlank(bytes[end - 1]!)) end--
|
|
409
|
+
type = isomorphic(bytes, position, end)
|
|
410
|
+
}
|
|
411
|
+
position = stop
|
|
412
|
+
}
|
|
413
|
+
if (bytes[position] !== CR || bytes[position + 1] !== LF) return undefined
|
|
414
|
+
position += 2
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* The parts of a body, read as the Fetch standard's `multipart/form-data` parser reads them: the
|
|
420
|
+
* body opens with `--boundary`, every part's content runs to the next CRLF `--boundary`, and the
|
|
421
|
+
* first `--boundary--` closes it. `at` holds every `--boundary` in the body; those that do not start
|
|
422
|
+
* a line are content. Undefined when the body is not such a form.
|
|
423
|
+
*/
|
|
424
|
+
function partsOf(bytes: Uint8Array, at: readonly number[], size: number): Part[] | undefined {
|
|
425
|
+
if (at[0] !== 0) return undefined
|
|
426
|
+
const parts: Part[] = []
|
|
427
|
+
let position = 0
|
|
428
|
+
let next = 0
|
|
429
|
+
for (;;) {
|
|
430
|
+
position += size
|
|
431
|
+
if (bytes[position] === DASH && bytes[position + 1] === DASH) return parts
|
|
432
|
+
if (bytes[position] !== CR || bytes[position + 1] !== LF) return undefined
|
|
433
|
+
const head = partHeaders(bytes, position + 2)
|
|
434
|
+
if (head === undefined) return undefined
|
|
435
|
+
position = head.next
|
|
436
|
+
let end: number
|
|
437
|
+
do end = at[++next] ?? -1
|
|
438
|
+
while (end !== -1 && (end - 2 < position || bytes[end - 2] !== CR || bytes[end - 1] !== LF))
|
|
439
|
+
if (end === -1) return undefined
|
|
440
|
+
parts.push({
|
|
441
|
+
name: head.name,
|
|
442
|
+
filename: head.filename,
|
|
443
|
+
type: head.type,
|
|
444
|
+
start: position,
|
|
445
|
+
end: end - 2,
|
|
446
|
+
})
|
|
447
|
+
position = end
|
|
282
448
|
}
|
|
283
|
-
return false
|
|
284
449
|
}
|
|
285
450
|
|
|
286
451
|
/** C0 controls, DEL, line separators, and the characters that reorder or hide text in a label. */
|
|
@@ -326,21 +491,6 @@ function safeFileName(raw: string): string {
|
|
|
326
491
|
return name === "" || name === "." || name === ".." ? FALLBACK_FILE_NAME : name
|
|
327
492
|
}
|
|
328
493
|
|
|
329
|
-
/** `part` as a `File` called `name`, keeping its bytes, type, and timestamp. */
|
|
330
|
-
function renamedFile(part: Blob, name: string): File {
|
|
331
|
-
const modified = (part as Partial<File>).lastModified
|
|
332
|
-
const options: FilePropertyBag =
|
|
333
|
-
modified === undefined ? { type: part.type } : { type: part.type, lastModified: modified }
|
|
334
|
-
let file = new File([part], name, options)
|
|
335
|
-
// A runtime may hand back the single source part with its own name when nothing else changes;
|
|
336
|
-
// a second, empty part forces a new file.
|
|
337
|
-
if (file.name !== name) file = new File([part, ""], name, options)
|
|
338
|
-
if (file.name !== name || file.size !== part.size) {
|
|
339
|
-
throw new Error("multipartBody: this runtime did not apply a file name")
|
|
340
|
-
}
|
|
341
|
-
return file
|
|
342
|
-
}
|
|
343
|
-
|
|
344
494
|
function createReader(
|
|
345
495
|
limits: MultipartLimits,
|
|
346
496
|
inner: SchemaBodyReader | undefined,
|
|
@@ -368,65 +518,47 @@ function createReader(
|
|
|
368
518
|
if (boundary === null) return INVALID()
|
|
369
519
|
|
|
370
520
|
const read = await readBoundedBytes(source, maxBodyBytes)
|
|
371
|
-
if (!read.ok)
|
|
372
|
-
return read.status === 413
|
|
373
|
-
? plainError(413, "payload_too_large")
|
|
374
|
-
: plainError(400, "invalid_content_length")
|
|
375
|
-
}
|
|
521
|
+
if (!read.ok) return plainError(read.status, read.error)
|
|
376
522
|
const bytes = read.bytes
|
|
377
523
|
|
|
378
|
-
// Counted on the raw bytes: the
|
|
379
|
-
//
|
|
524
|
+
// Counted on the raw bytes: the parser allocates per part, so a body of many tiny parts must be
|
|
525
|
+
// refused before it is parsed, not after.
|
|
380
526
|
const delimiter = new Uint8Array(boundary.length + 2)
|
|
381
527
|
delimiter[0] = DASH
|
|
382
528
|
delimiter[1] = DASH
|
|
383
529
|
for (let i = 0; i < boundary.length; i++) delimiter[i + 2] = boundary.charCodeAt(i)
|
|
384
530
|
const found = scanDelimiters(bytes, delimiter, delimiterLimits)
|
|
385
|
-
if (overLimits(found.
|
|
531
|
+
if (overLimits(found.at.length, found.empty, delimiterLimits)) {
|
|
386
532
|
return plainError(413, "too_many_parts")
|
|
387
533
|
}
|
|
388
|
-
// A body
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
// The parser is given the boundary this reader extracted, never the request's own header: two
|
|
393
|
-
// readings of one header (a repeated or oddly quoted parameter) would let a body be counted
|
|
394
|
-
// under one boundary and parsed under another.
|
|
395
|
-
const canonical = `${MEDIA_TYPE}; boundary=${TOKEN.test(boundary) ? boundary : `"${boundary}"`}`
|
|
396
|
-
// Typed by what is read from it: the global `FormData` and `BodyInit` names do not resolve to
|
|
397
|
-
// the same declarations under every lib config.
|
|
398
|
-
let form: Iterable<[string, string | Blob]>
|
|
399
|
-
try {
|
|
400
|
-
form = (await new Response(bytes as ConstructorParameters<typeof Response>[0], {
|
|
401
|
-
headers: { "content-type": canonical },
|
|
402
|
-
}).formData()) as unknown as Iterable<[string, string | Blob]>
|
|
403
|
-
} catch {
|
|
404
|
-
return INVALID()
|
|
405
|
-
}
|
|
534
|
+
// A body that stops before `--boundary--` is refused with the rest, because a reader that
|
|
535
|
+
// tolerated it would hand back the parts it saw as if they were the whole form.
|
|
536
|
+
const parts = partsOf(bytes, found.at, delimiter.length)
|
|
537
|
+
if (parts === undefined) return INVALID()
|
|
406
538
|
|
|
407
539
|
const record = Object.create(null) as Record<string, MultipartValue>
|
|
408
540
|
let fields = 0
|
|
409
541
|
let files = 0
|
|
410
|
-
for (const
|
|
542
|
+
for (const part of parts) {
|
|
543
|
+
const name = part.name
|
|
411
544
|
if (name === "__proto__" && protoPoisoning !== "ignore") {
|
|
412
545
|
if (protoPoisoning === "reject") return INVALID()
|
|
413
546
|
continue
|
|
414
547
|
}
|
|
548
|
+
const size = part.end - part.start
|
|
415
549
|
let value: string | File
|
|
416
|
-
if (
|
|
550
|
+
if (part.filename === null) {
|
|
417
551
|
if (++fields > maxFields) return plainError(413, "too_many_fields")
|
|
418
|
-
if (
|
|
419
|
-
value = part
|
|
552
|
+
if (size > maxFieldBytes) return plainError(413, "field_too_large")
|
|
553
|
+
value = textOf(bytes, part.start, part.end)
|
|
420
554
|
} else {
|
|
421
|
-
// A file input left empty still submits a part with no bytes
|
|
422
|
-
|
|
423
|
-
if (part.size === 0) continue
|
|
424
|
-
const rawName = (part as Partial<File>).name
|
|
425
|
-
const clientName = typeof rawName === "string" ? rawName : ""
|
|
555
|
+
// A file input left empty still submits a part with no bytes, so an empty file is that input.
|
|
556
|
+
if (size === 0) continue
|
|
426
557
|
if (++files > maxFiles) return plainError(413, "too_many_files")
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
|
|
558
|
+
if (size > maxFileBytes) return plainError(413, "file_too_large")
|
|
559
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: readBoundedBytes allocates its own ArrayBuffer, never a shared one.
|
|
560
|
+
const content = bytes.subarray(part.start, part.end) as Uint8Array<ArrayBuffer>
|
|
561
|
+
value = fileOf(content, safeFileName(part.filename), part.type ?? "text/plain")
|
|
430
562
|
}
|
|
431
563
|
const existing = record[name]
|
|
432
564
|
if (existing === undefined) record[name] = value
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* conversion. Mirrors `respond.ts` but skips the undici `Response` build where the adapter can write a
|
|
4
4
|
* plain-data render straight to the socket. Imports only runtime-core + respond + the spine types.
|
|
5
5
|
*/
|
|
6
|
+
|
|
7
|
+
import { jsonText } from "./json-text.ts"
|
|
6
8
|
import {
|
|
7
9
|
applySetToResponse,
|
|
8
10
|
applyStaticResponseHeaders,
|
|
@@ -88,7 +90,7 @@ export function toNodeOutcome(result: HandlerResult, set: CtxSet): NodeServeOutc
|
|
|
88
90
|
return nodeOutcomeFromResponse(applySetToResponse(normalizeBodylessResponse(result), set))
|
|
89
91
|
}
|
|
90
92
|
const status = set.status ?? (result === undefined ? 204 : 200)
|
|
91
|
-
const body = result === undefined || isBodylessStatus(status) ? null :
|
|
93
|
+
const body = result === undefined || isBodylessStatus(status) ? null : jsonText(result)
|
|
92
94
|
return {
|
|
93
95
|
kind: "json",
|
|
94
96
|
status,
|
|
@@ -109,10 +111,7 @@ export function plainNodeOutcome(plain: PlainRender, set: CtxSet): NodeServeOutc
|
|
|
109
111
|
status: plain.status,
|
|
110
112
|
headers: plainRenderHeaders(plain, set),
|
|
111
113
|
cookies: set._cookies,
|
|
112
|
-
body:
|
|
113
|
-
plain.body === undefined || isBodylessStatus(plain.status)
|
|
114
|
-
? null
|
|
115
|
-
: JSON.stringify(plain.body),
|
|
114
|
+
body: plain.body === undefined || isBodylessStatus(plain.status) ? null : jsonText(plain.body),
|
|
116
115
|
}
|
|
117
116
|
}
|
|
118
117
|
|
|
@@ -49,12 +49,37 @@ export function guardParsedValue(value: unknown, policy: ProtoPoisoning): unknow
|
|
|
49
49
|
*/
|
|
50
50
|
/** Reused walk stack - `sweep` is synchronous and single-threaded, and a thrown rejection can
|
|
51
51
|
* leave residue, so each entry clears it. Saves one array allocation per swept request. */
|
|
52
|
-
const
|
|
52
|
+
const STORAGE: unknown[] = []
|
|
53
|
+
// Reuse at most 1,024 logical slots between requests; larger walks use the existing full reset.
|
|
54
|
+
const MAX_REUSED_STACK = 1024
|
|
55
|
+
// JSC keeps native Array.pop on this hot path; a JS cursor lets it inline the stack operations.
|
|
56
|
+
// Select once so V8 keeps its faster native array stack, with one shared security walk below.
|
|
57
|
+
const INDEXED_STACK = typeof Bun !== "undefined"
|
|
58
|
+
const STACK = !INDEXED_STACK
|
|
59
|
+
? STORAGE
|
|
60
|
+
: {
|
|
61
|
+
length: 0,
|
|
62
|
+
push(value: unknown): void {
|
|
63
|
+
STORAGE[this.length++] = value
|
|
64
|
+
},
|
|
65
|
+
pop(): unknown {
|
|
66
|
+
const index = --this.length
|
|
67
|
+
const value = STORAGE[index]
|
|
68
|
+
STORAGE[index] = undefined
|
|
69
|
+
return value
|
|
70
|
+
},
|
|
71
|
+
}
|
|
53
72
|
|
|
54
73
|
function sweep(root: unknown, policy: "reject" | "strip"): unknown {
|
|
55
74
|
if (root === null || typeof root !== "object") return root
|
|
56
75
|
const stack = STACK
|
|
57
|
-
|
|
76
|
+
// Keep V8's native-array reset: skipping it regresses the deep-chain control. Bun only needs
|
|
77
|
+
// a reset after an unexpected custom-source throw; ordinary pops already cleared every slot.
|
|
78
|
+
if (!INDEXED_STACK) stack.length = 0
|
|
79
|
+
else if (stack.length > 0) {
|
|
80
|
+
stack.length = 0
|
|
81
|
+
STORAGE.length = 0
|
|
82
|
+
}
|
|
58
83
|
stack.push(root)
|
|
59
84
|
while (stack.length > 0) {
|
|
60
85
|
const node = stack.pop() as object
|
|
@@ -95,7 +120,11 @@ function sweep(root: unknown, policy: "reject" | "strip"): unknown {
|
|
|
95
120
|
// + first-code-unit guards avoid a full string comparison for the overwhelming majority of
|
|
96
121
|
// fields while preserving exact equality for the two security-sensitive names.
|
|
97
122
|
if (key.length === 9 && key.charCodeAt(0) === 95 /* _ */ && key === "__proto__") {
|
|
98
|
-
if (policy === "reject")
|
|
123
|
+
if (policy === "reject") {
|
|
124
|
+
stack.length = 0
|
|
125
|
+
STORAGE.length = 0
|
|
126
|
+
throw POISONED
|
|
127
|
+
}
|
|
99
128
|
// biome-ignore lint/complexity/useLiteralKeys: bracket access keeps the guarded key an explicit string, never a prototype walk
|
|
100
129
|
delete record["__proto__"]
|
|
101
130
|
continue
|
|
@@ -107,7 +136,11 @@ function sweep(root: unknown, policy: "reject" | "strip"): unknown {
|
|
|
107
136
|
nested &&
|
|
108
137
|
Object.hasOwn(value, "prototype")
|
|
109
138
|
) {
|
|
110
|
-
if (policy === "reject")
|
|
139
|
+
if (policy === "reject") {
|
|
140
|
+
stack.length = 0
|
|
141
|
+
STORAGE.length = 0
|
|
142
|
+
throw POISONED
|
|
143
|
+
}
|
|
111
144
|
// biome-ignore lint/complexity/useLiteralKeys: bracket access keeps the guarded key an explicit string, never a prototype walk
|
|
112
145
|
delete record["constructor"]
|
|
113
146
|
continue
|
|
@@ -115,6 +148,8 @@ function sweep(root: unknown, policy: "reject" | "strip"): unknown {
|
|
|
115
148
|
if (nested) stack.push(value)
|
|
116
149
|
}
|
|
117
150
|
}
|
|
151
|
+
// Bun pops clear their references immediately, so ordinary requests can reuse the empty buffer.
|
|
152
|
+
if (INDEXED_STACK && STORAGE.length > MAX_REUSED_STACK) STORAGE.length = 0
|
|
118
153
|
return root
|
|
119
154
|
}
|
|
120
155
|
|
package/src/server/query.ts
CHANGED
|
@@ -64,12 +64,11 @@ function setQueryValue(out: Record<string, QueryValue>, rawKey: string, value: s
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function queryObjectFallback(search: string): Record<string, QueryValue> {
|
|
67
|
-
//
|
|
68
|
-
// (fromEntries is last-wins), and __proto__ needs the same own-property guard.
|
|
67
|
+
// Native forEach avoids iterator/pair arrays while preserving repeated-key order and safe keys.
|
|
69
68
|
const out: Record<string, QueryValue> = Object.create(null) as Record<string, QueryValue>
|
|
70
|
-
|
|
69
|
+
new URLSearchParams(search).forEach((value, key) => {
|
|
71
70
|
setQueryValue(out, key, value)
|
|
72
|
-
}
|
|
71
|
+
})
|
|
73
72
|
return out
|
|
74
73
|
}
|
|
75
74
|
|
|
@@ -87,10 +86,9 @@ export function queryObjectOf(search: string): Record<string, QueryValue> {
|
|
|
87
86
|
const start = search.charCodeAt(0) === 63 /* ? */ ? 1 : 0
|
|
88
87
|
if (start >= search.length) return EMPTY_QUERY
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
89
|
+
// Native single-character searches avoid a JS pass over every unencoded value byte.
|
|
90
|
+
if (search.indexOf("+", start) !== -1 || search.indexOf("%", start) !== -1)
|
|
91
|
+
return queryObjectFallback(search)
|
|
94
92
|
|
|
95
93
|
const out: Record<string, QueryValue> = Object.create(null) as Record<string, QueryValue>
|
|
96
94
|
let pos = start
|
|
@@ -191,11 +189,7 @@ export async function readBoundedForm(
|
|
|
191
189
|
maxBytes: number,
|
|
192
190
|
): Promise<Record<string, QueryValue> | ResponseResult> {
|
|
193
191
|
const read = await readBoundedBytes(req, maxBytes)
|
|
194
|
-
if (!read.ok)
|
|
195
|
-
return read.status === 413
|
|
196
|
-
? plainError(413, "payload_too_large")
|
|
197
|
-
: plainError(400, "invalid_content_length")
|
|
198
|
-
}
|
|
192
|
+
if (!read.ok) return plainError(read.status, read.error)
|
|
199
193
|
const out: Record<string, QueryValue> = Object.create(null) as Record<string, QueryValue>
|
|
200
194
|
// URLSearchParams owns the format's quirks (`+` as space, percent-decoding, empty keys);
|
|
201
195
|
// it never throws on junk input, so no try/catch is needed here.
|