@znight/http 0.1.2 → 0.1.3
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 +79 -32
- package/dist/compiler.mjs +82 -13
- package/dist/compiler.mjs.map +1 -1
- package/dist/express.mjs +3 -2
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.mjs +200 -23
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.mjs +174 -43
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +9 -1
- package/dist/index.mjs +813 -255
- package/dist/index.mjs.map +1 -1
- package/dist/{node-Clm1Ia7T.mjs → node-DnYv_AMI.mjs} +4 -3
- package/dist/{node-Clm1Ia7T.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
- package/dist/{node-io-CVnqpVNS.mjs → node-io-BkdRSQeg.mjs} +99 -60
- package/dist/{node-io-CVnqpVNS.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
- package/dist/node.mjs +1 -1
- package/dist/utils-DIWdzVht.mjs +46 -0
- package/dist/utils-DIWdzVht.mjs.map +1 -0
- package/package.json +4 -4
- package/src/adapters/express.ts +4 -1
- package/src/adapters/fastify.ts +265 -25
- package/src/adapters/hono.ts +225 -58
- package/src/adapters/node-io.ts +128 -58
- package/src/adapters/node.ts +4 -1
- package/src/body.ts +31 -2
- package/src/coerce.ts +31 -0
- package/src/compiler/generate.ts +127 -22
- package/src/context.ts +148 -78
- package/src/index.ts +1 -1
- package/src/pipeline.ts +714 -176
- package/src/plugin.ts +1 -2
- package/src/response.ts +6 -8
- package/src/serialize.ts +48 -21
- package/src/utils.ts +25 -2
- package/src/validate.ts +28 -12
- package/dist/utils-DCbu3a4w.mjs +0 -27
- package/dist/utils-DCbu3a4w.mjs.map +0 -1
package/src/adapters/hono.ts
CHANGED
|
@@ -22,11 +22,20 @@ import type { HeaderValue } from '../exceptions.ts'
|
|
|
22
22
|
|
|
23
23
|
import { type Context, Hono, type Next } from 'hono'
|
|
24
24
|
|
|
25
|
+
import { BadRequestException } from '../exceptions.ts'
|
|
25
26
|
import { toHonoPath } from '../path.ts'
|
|
26
27
|
import { discardStream } from '../stream.ts'
|
|
27
28
|
import { isBun } from '../utils.ts'
|
|
28
29
|
import { ConnectionTracker, closeServer, nodeBodyStream, onResponseAbort, readNodeBody } from './node-io.ts'
|
|
29
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
bufferInjectStream,
|
|
32
|
+
exceedsDeclaredLength,
|
|
33
|
+
injectFetchStream,
|
|
34
|
+
payloadTooLarge,
|
|
35
|
+
readWebStream,
|
|
36
|
+
serverAddress,
|
|
37
|
+
webBodyStream,
|
|
38
|
+
} from './shared.ts'
|
|
30
39
|
|
|
31
40
|
/** Options of {@link honoAdapter}. */
|
|
32
41
|
export interface HonoAdapterOptions {
|
|
@@ -103,12 +112,47 @@ interface BunRuntime {
|
|
|
103
112
|
/** Default `idleTimeout` of `Bun.serve()`, in seconds. */
|
|
104
113
|
const BUN_IDLE_TIMEOUT = 10
|
|
105
114
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
115
|
+
/**
|
|
116
|
+
* A streamed response body built for a request of `Bun.serve()` (see `#bunStream`); with middleware,
|
|
117
|
+
* pending until `#bunFetch` takes it or it ends.
|
|
118
|
+
*/
|
|
119
|
+
class BunBody {
|
|
108
120
|
/** The `Blob` (file) the body reads. */
|
|
109
121
|
readonly blob: Blob | undefined
|
|
110
|
-
|
|
111
|
-
onEnd
|
|
122
|
+
#settle: (() => void) | undefined
|
|
123
|
+
#onEnd: (() => void) | undefined
|
|
124
|
+
#ended = false
|
|
125
|
+
|
|
126
|
+
/** `settle` is called once, when the body is taken by `#bunFetch` or ends, whichever comes first. */
|
|
127
|
+
constructor(blob: Blob | undefined, settle: (() => void) | undefined) {
|
|
128
|
+
this.blob = blob
|
|
129
|
+
this.#settle = settle
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Whether the body ended (read to its end, failed or cancelled). */
|
|
133
|
+
get ended(): boolean {
|
|
134
|
+
return this.#ended
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The body is no longer pending (taken by `#bunFetch`). */
|
|
138
|
+
settle(): void {
|
|
139
|
+
const settle = this.#settle
|
|
140
|
+
this.#settle = undefined
|
|
141
|
+
settle?.()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Sets the function called when the body ends. */
|
|
145
|
+
onEnd(callback: () => void): void {
|
|
146
|
+
this.#onEnd = callback
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The body ended (read to its end, failed or cancelled). */
|
|
150
|
+
end(): void {
|
|
151
|
+
if (this.#ended) return
|
|
152
|
+
this.#ended = true
|
|
153
|
+
this.settle()
|
|
154
|
+
this.#onEnd?.()
|
|
155
|
+
}
|
|
112
156
|
}
|
|
113
157
|
|
|
114
158
|
/** Response bodies built while serving with `Bun.serve()`, by body (Hono may copy a `Response`, not its body). */
|
|
@@ -127,15 +171,31 @@ const INJECT_ENV = Object.freeze({ incoming: Object.freeze({ socket: Object.free
|
|
|
127
171
|
|
|
128
172
|
type HonoMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'
|
|
129
173
|
|
|
174
|
+
/** The handlers of the GET and HEAD routes of one path. */
|
|
175
|
+
interface ReadHandlers {
|
|
176
|
+
get: AdapterHandler | undefined
|
|
177
|
+
head: AdapterHandler | undefined
|
|
178
|
+
}
|
|
179
|
+
|
|
130
180
|
class HonoHttpAdapter implements HonoAdapter {
|
|
131
181
|
readonly name = 'hono' as const
|
|
132
182
|
readonly app: Hono
|
|
133
183
|
readonly #serveOptions: HonoServeOptions
|
|
134
184
|
/** Number of routes of `app` registered by the adapter (see {@link #middleware}). */
|
|
135
185
|
#routes = 0
|
|
186
|
+
/** The GET and HEAD handlers of each path shape, served by one route of `app` (see {@link registerRoute}). */
|
|
187
|
+
readonly #reads = new Map<string, ReadHandlers>()
|
|
136
188
|
#fallback: FallbackHandler | undefined
|
|
137
189
|
#prepared = false
|
|
138
190
|
#bun: BunServer | undefined
|
|
191
|
+
/**
|
|
192
|
+
* Streamed bodies of `Bun.serve()` replies (with middleware) neither taken by `#bunFetch` nor ended
|
|
193
|
+
* (see {@link BunBody}): while there is none, `#bunFetch` returns responses untouched (reading
|
|
194
|
+
* `response.body` would turn a text body into a stream that Bun then sends as one).
|
|
195
|
+
*/
|
|
196
|
+
#pendingBodies = 0
|
|
197
|
+
/** Idle timeout of the `Bun.serve()` server, in seconds (`0`: none). */
|
|
198
|
+
#idleTimeout = BUN_IDLE_TIMEOUT
|
|
139
199
|
#node: Server | undefined
|
|
140
200
|
#tracker: ConnectionTracker | undefined
|
|
141
201
|
#closing: Promise<void> | undefined
|
|
@@ -153,14 +213,29 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
153
213
|
registerRoute(route: AdapterRoute): void {
|
|
154
214
|
const handler = route.handler
|
|
155
215
|
const path = toHonoPath(route.path)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
216
|
+
if (route.method === 'GET' || route.method === 'HEAD') {
|
|
217
|
+
// Hono routes HEAD requests to GET handlers: one GET handler per path serves both methods (the
|
|
218
|
+
// plugin registers HEAD routes first), so that a request matches a single handler and Hono
|
|
219
|
+
// skips its middleware composition. Paths are keyed by their shape (`/users/:` for
|
|
220
|
+
// `/users/:id` and `/users/:userId`): a GET route joins the HEAD route matching the same
|
|
221
|
+
// requests, at its rank (Znight reads the parameters itself, never Hono's names)
|
|
222
|
+
const existing = this.#reads.get(route.path.shape)
|
|
223
|
+
if (existing !== undefined) {
|
|
224
|
+
if (route.method === 'GET') existing.get = handler
|
|
225
|
+
else existing.head = handler
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
const reads: ReadHandlers =
|
|
229
|
+
route.method === 'GET' ? { get: handler, head: undefined } : { get: undefined, head: handler }
|
|
230
|
+
this.#reads.set(route.path.shape, reads)
|
|
231
|
+
this.#routes++
|
|
232
|
+
this.app.get(path, (c: Context, next: Next) => {
|
|
233
|
+
const read = c.req.raw.method === 'HEAD' ? reads.head : reads.get
|
|
234
|
+
return read === undefined ? next().then(() => c.res) : this.#serve(c, read)
|
|
235
|
+
})
|
|
162
236
|
return
|
|
163
237
|
}
|
|
238
|
+
this.#routes++
|
|
164
239
|
this.app.on(route.method as HonoMethod, path, (c: Context) => this.#serve(c, handler))
|
|
165
240
|
}
|
|
166
241
|
|
|
@@ -176,11 +251,12 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
176
251
|
}
|
|
177
252
|
|
|
178
253
|
async #serve(c: Context, handler: AdapterHandler): Promise<Response> {
|
|
179
|
-
const request =
|
|
254
|
+
const request = this.#request(c)
|
|
180
255
|
// Hono routes the WHATWG-normalised URL (`/a/../b` → `/b`, `%2e%2e` resolved): when the raw
|
|
181
|
-
// target differs (Node.js), let the plugin route the raw
|
|
256
|
+
// target differs (Node.js; on Bun only the normalised URL is known), let the plugin route the raw
|
|
257
|
+
// path, like on the other adapters
|
|
182
258
|
const reply =
|
|
183
|
-
this.#fallback !== undefined && pathOf(request.url) !== pathOf(pathAndQuery(c.req.raw.url))
|
|
259
|
+
this.#fallback !== undefined && request.rawTarget && pathOf(request.url) !== pathOf(pathAndQuery(c.req.raw.url))
|
|
184
260
|
? await this.#fallback(request)
|
|
185
261
|
: await handler(request)
|
|
186
262
|
return this.#toResponse(c, reply)
|
|
@@ -188,7 +264,16 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
188
264
|
|
|
189
265
|
async #serveFallback(c: Context, error: unknown): Promise<Response> {
|
|
190
266
|
if (this.#fallback === undefined) throw error ?? new Error('No fallback handler')
|
|
191
|
-
return this.#toResponse(c, await this.#fallback(
|
|
267
|
+
return this.#toResponse(c, await this.#fallback(this.#request(c), error))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#request(c: Context): HonoAdapterRequest {
|
|
271
|
+
return new HonoAdapterRequest(c, this.#servedByBun(c) ? this.#bun : undefined)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Whether the request of `c` was received by the `Bun.serve()` server of the adapter. */
|
|
275
|
+
#servedByBun(c: Context): boolean {
|
|
276
|
+
return this.#bun !== undefined && c.env === this.#bun
|
|
192
277
|
}
|
|
193
278
|
|
|
194
279
|
/**
|
|
@@ -201,6 +286,11 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
201
286
|
|
|
202
287
|
#toResponse(c: Context, reply: AdapterReply): Response {
|
|
203
288
|
const middleware = this.#middleware()
|
|
289
|
+
if (this.#servedByBun(c)) {
|
|
290
|
+
return reply.body instanceof ReadableStream
|
|
291
|
+
? this.#bunStream(c, reply, middleware)
|
|
292
|
+
: toResponse(c, reply, middleware)
|
|
293
|
+
}
|
|
204
294
|
const outgoing = nodeOutgoing(c)
|
|
205
295
|
if (outgoing !== undefined) {
|
|
206
296
|
const body = reply.body instanceof ReadableStream ? reply.body : null
|
|
@@ -220,16 +310,23 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
220
310
|
// a quiet event stream must outlive a socket timeout
|
|
221
311
|
if (reply.eventStream === true && typeof outgoing.setTimeout === 'function') outgoing.setTimeout(0)
|
|
222
312
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
313
|
+
return toResponse(c, reply, middleware)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The response of a streamed reply to a request of `Bun.serve()`: the end of its body is observed
|
|
318
|
+
* (idle timeout, native blob: see {@link #bunSend}).
|
|
319
|
+
*/
|
|
320
|
+
#bunStream(c: Context, reply: AdapterReply, middleware: boolean): Response {
|
|
321
|
+
const reader = (reply.body as ReadableStream<Uint8Array>).getReader()
|
|
322
|
+
const served = new BunBody(
|
|
323
|
+
reply.blob,
|
|
324
|
+
middleware
|
|
325
|
+
? () => {
|
|
326
|
+
this.#pendingBodies--
|
|
327
|
+
}
|
|
328
|
+
: undefined,
|
|
329
|
+
)
|
|
233
330
|
const body = new ReadableStream<Uint8Array>(
|
|
234
331
|
{
|
|
235
332
|
async pull(controller) {
|
|
@@ -237,39 +334,53 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
237
334
|
try {
|
|
238
335
|
result = await reader.read()
|
|
239
336
|
} catch (error) {
|
|
240
|
-
end()
|
|
337
|
+
served.end()
|
|
241
338
|
throw error
|
|
242
339
|
}
|
|
243
340
|
if (result.done) {
|
|
244
|
-
end() // before closing: Bun ignores timeout() once the response is complete
|
|
341
|
+
served.end() // before closing: Bun ignores timeout() once the response is complete
|
|
245
342
|
controller.close()
|
|
246
343
|
return
|
|
247
344
|
}
|
|
248
345
|
controller.enqueue(result.value)
|
|
249
346
|
},
|
|
250
347
|
cancel(reason) {
|
|
251
|
-
end()
|
|
348
|
+
served.end()
|
|
252
349
|
return reader.cancel(reason)
|
|
253
350
|
},
|
|
254
351
|
},
|
|
255
352
|
{ highWaterMark: 0 },
|
|
256
353
|
)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
354
|
+
const response = toResponse(c, { ...reply, body }, middleware)
|
|
355
|
+
if (!middleware) {
|
|
356
|
+
// nothing else handles the request (see the fetch handler given to Bun.serve()): Bun sends this
|
|
357
|
+
// response, unless it does not carry the body (an invalid header: the body was discarded)
|
|
358
|
+
return served.ended ? response : this.#bunSend(response, body, served, c.req.raw)
|
|
359
|
+
}
|
|
360
|
+
// middleware may copy the response (not its body) or replace it: #bunFetch finds the body
|
|
361
|
+
this.#pendingBodies++
|
|
362
|
+
BUN_BODIES.set(body, served)
|
|
363
|
+
return response
|
|
264
364
|
}
|
|
265
365
|
|
|
266
|
-
/** The fetch handler given to `Bun.serve()
|
|
267
|
-
async #bunFetch(request: Request, server: BunServer
|
|
366
|
+
/** The fetch handler given to `Bun.serve()` when `app` has middleware (see {@link #bunStream}). */
|
|
367
|
+
async #bunFetch(request: Request, server: BunServer): Promise<Response> {
|
|
268
368
|
const response = await this.app.fetch(request, server)
|
|
369
|
+
// only a streamed reply needs more: leave the other responses untouched (see #pendingBodies)
|
|
370
|
+
if (this.#pendingBodies === 0) return response
|
|
269
371
|
const body = response.body
|
|
270
372
|
const served = body === null ? undefined : BUN_BODIES.get(body)
|
|
271
373
|
if (body === null || served === undefined) return response
|
|
272
374
|
BUN_BODIES.delete(body)
|
|
375
|
+
served.settle()
|
|
376
|
+
return this.#bunSend(response, body, served, request)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The response Bun sends for a streamed reply, once final: a file `Blob` is sent natively, a
|
|
381
|
+
* stream with the idle timeout of the connection lifted.
|
|
382
|
+
*/
|
|
383
|
+
#bunSend(response: Response, body: ReadableStream<Uint8Array>, served: BunBody, request: Request): Response {
|
|
273
384
|
if (served.blob !== undefined && !body.locked) {
|
|
274
385
|
// a Blob is sent by Bun itself (sendfile, exact Content-Length; a stream would be chunked). Bun
|
|
275
386
|
// opens its file as soon as it takes the response: release the stream (and with it the request
|
|
@@ -282,9 +393,12 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
282
393
|
setImmediate(() => discardStream(body))
|
|
283
394
|
return native
|
|
284
395
|
}
|
|
285
|
-
|
|
396
|
+
const server = this.#bun
|
|
397
|
+
const idleTimeout = this.#idleTimeout
|
|
398
|
+
if (idleTimeout > 0 && !served.ended && typeof server?.timeout === 'function') {
|
|
286
399
|
// the idle timeout of Bun.serve() also cuts a response whose stream pauses (SSE, slow producers):
|
|
287
|
-
// lift it while the stream is sent, then restore it for the keep-alive connection
|
|
400
|
+
// lift it while the stream is sent, then restore it for the keep-alive connection (a body that
|
|
401
|
+
// already ended would never restore it)
|
|
288
402
|
server.timeout(request, 0)
|
|
289
403
|
served.onEnd(() => {
|
|
290
404
|
try {
|
|
@@ -303,14 +417,16 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
303
417
|
const bun = (globalThis as { Bun?: BunRuntime }).Bun
|
|
304
418
|
if (isBun() && bun !== undefined) {
|
|
305
419
|
const options = this.#serveOptions
|
|
306
|
-
|
|
420
|
+
this.#idleTimeout = typeof options.idleTimeout === 'number' ? options.idleTimeout : BUN_IDLE_TIMEOUT
|
|
307
421
|
const server = bun.serve({
|
|
308
422
|
// Znight enforces the body limits (route @BodyLimit, plugin bodyLimit): not Bun's 128 MiB default
|
|
309
423
|
maxRequestBodySize: Number.MAX_SAFE_INTEGER,
|
|
310
424
|
...options,
|
|
311
425
|
port,
|
|
312
426
|
hostname: host,
|
|
313
|
-
|
|
427
|
+
// without middleware, the response of a Znight route is the one Bun sends (no wrapper needed)
|
|
428
|
+
fetch: (request, server) =>
|
|
429
|
+
this.#middleware() ? this.#bunFetch(request, server) : this.app.fetch(request, server),
|
|
314
430
|
})
|
|
315
431
|
this.#bun = server
|
|
316
432
|
return serverAddress(server.hostname, server.port)
|
|
@@ -367,10 +483,6 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
367
483
|
}
|
|
368
484
|
}
|
|
369
485
|
|
|
370
|
-
function adapterRequest(c: Context): AdapterRequest {
|
|
371
|
-
return new HonoAdapterRequest(c)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
486
|
/** A Hono request as an {@link AdapterRequest} (a class: no closure per request). */
|
|
375
487
|
class HonoAdapterRequest implements AdapterRequest {
|
|
376
488
|
readonly method: string
|
|
@@ -381,6 +493,8 @@ class HonoAdapterRequest implements AdapterRequest {
|
|
|
381
493
|
readonly encrypted: boolean
|
|
382
494
|
readonly raw: Request
|
|
383
495
|
readonly rawResponse: Context
|
|
496
|
+
/** `url` is the raw request target (Node.js), not the WHATWG-normalised URL of `raw`. */
|
|
497
|
+
readonly rawTarget: boolean
|
|
384
498
|
/** The Node.js request of `@hono/node-server`, absent on Bun and for `inject()`. */
|
|
385
499
|
readonly #incoming: NodeIncoming | undefined
|
|
386
500
|
/**
|
|
@@ -388,22 +502,42 @@ class HonoAdapterRequest implements AdapterRequest {
|
|
|
388
502
|
* stream are built for it.
|
|
389
503
|
*/
|
|
390
504
|
readonly #http1: IncomingMessage | undefined
|
|
505
|
+
/** Received by the `Bun.serve()` server of the adapter: its body is framed by the HTTP parser. */
|
|
506
|
+
readonly #bun: boolean
|
|
391
507
|
#body: Promise<Uint8Array> | undefined
|
|
392
508
|
|
|
393
|
-
constructor(c: Context) {
|
|
509
|
+
constructor(c: Context, bun: BunServer | undefined) {
|
|
394
510
|
const request = c.req.raw
|
|
511
|
+
this.method = request.method
|
|
512
|
+
this.raw = request
|
|
513
|
+
this.rawResponse = c
|
|
514
|
+
this.#bun = bun !== undefined
|
|
515
|
+
if (bun !== undefined) {
|
|
516
|
+
// Bun.serve(): only the normalised URL is known; the address is asked now (Bun no longer knows
|
|
517
|
+
// it once the response is sent)
|
|
518
|
+
const url = request.url
|
|
519
|
+
const address = bun.requestIP?.(request)?.address ?? undefined
|
|
520
|
+
this.url = pathAndQuery(url)
|
|
521
|
+
this.rawTarget = false
|
|
522
|
+
this.headers = fetchHeaders(request.headers)
|
|
523
|
+
this.ip = address
|
|
524
|
+
this.remoteAddress = address
|
|
525
|
+
this.encrypted = url.startsWith('https:')
|
|
526
|
+
this.#incoming = undefined
|
|
527
|
+
this.#http1 = undefined
|
|
528
|
+
return
|
|
529
|
+
}
|
|
395
530
|
const incoming = nodeIncoming(c)
|
|
396
531
|
const http1 = incoming !== undefined && incoming.httpVersionMajor < 2 ? (incoming as IncomingMessage) : undefined
|
|
397
532
|
const address = remoteAddress(c)
|
|
398
|
-
|
|
399
|
-
// the raw target on Node.js;
|
|
400
|
-
this.
|
|
533
|
+
const target = incoming?.url
|
|
534
|
+
// the raw target on Node.js; for a Web request only the normalised URL is known
|
|
535
|
+
this.rawTarget = typeof target === 'string' && target !== ''
|
|
536
|
+
this.url = pathAndQuery(this.rawTarget ? (target as string) : request.url)
|
|
401
537
|
this.headers = (http1 === undefined ? undefined : distinctHeaders(http1)) ?? fetchHeaders(request.headers)
|
|
402
538
|
this.ip = address
|
|
403
539
|
this.remoteAddress = address
|
|
404
540
|
this.encrypted = incoming === undefined ? request.url.startsWith('https:') : incoming.socket?.encrypted === true
|
|
405
|
-
this.raw = request
|
|
406
|
-
this.rawResponse = c
|
|
407
541
|
this.#incoming = incoming
|
|
408
542
|
this.#http1 = http1
|
|
409
543
|
}
|
|
@@ -411,14 +545,22 @@ class HonoAdapterRequest implements AdapterRequest {
|
|
|
411
545
|
readBody(limit: number): Promise<Uint8Array> {
|
|
412
546
|
if (this.#body === undefined) {
|
|
413
547
|
const http1 = this.#http1
|
|
414
|
-
this.#body =
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
: readNodeBody(http1, limit)
|
|
548
|
+
if (http1 !== undefined) this.#body = readNodeBody(http1, limit)
|
|
549
|
+
else if (this.#bun && this.#framed()) this.#body = readFramedBody(this.raw, limit, this.headers['content-length'])
|
|
550
|
+
else this.#body = readWebStream(() => this.raw.body, limit, this.headers['content-length'])
|
|
418
551
|
}
|
|
419
552
|
return this.#body
|
|
420
553
|
}
|
|
421
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Whether the whole body can be read at once from `Bun.serve()`: its length is declared (the
|
|
557
|
+
* HTTP parser then reads no more) and nothing read it yet.
|
|
558
|
+
*/
|
|
559
|
+
#framed(): boolean {
|
|
560
|
+
const headers = this.headers
|
|
561
|
+
return headers['content-length'] !== undefined && headers['transfer-encoding'] === undefined && !this.raw.bodyUsed
|
|
562
|
+
}
|
|
563
|
+
|
|
422
564
|
streamBody(limit: number): ReadableStream<Uint8Array> {
|
|
423
565
|
const http1 = this.#http1
|
|
424
566
|
if (http1 !== undefined) return nodeBodyStream(http1, limit)
|
|
@@ -436,7 +578,7 @@ class HonoAdapterRequest implements AdapterRequest {
|
|
|
436
578
|
}
|
|
437
579
|
|
|
438
580
|
onAbort(listener: () => void): void {
|
|
439
|
-
const outgoing = nodeOutgoing(this.rawResponse)
|
|
581
|
+
const outgoing = this.#bun ? undefined : nodeOutgoing(this.rawResponse)
|
|
440
582
|
if (outgoing !== undefined) {
|
|
441
583
|
onResponseAbort(outgoing as unknown as ServerResponse, listener)
|
|
442
584
|
return
|
|
@@ -451,10 +593,31 @@ class HonoAdapterRequest implements AdapterRequest {
|
|
|
451
593
|
/** The headers of a Fetch request: lower-case names, repeated headers joined (`Headers` semantics). */
|
|
452
594
|
function fetchHeaders(source: Headers): Record<string, string> {
|
|
453
595
|
const headers: Record<string, string> = {}
|
|
454
|
-
|
|
596
|
+
// forEach: no iterator nor entry array per header
|
|
597
|
+
source.forEach((value, name) => {
|
|
598
|
+
headers[name] = value
|
|
599
|
+
})
|
|
455
600
|
return headers
|
|
456
601
|
}
|
|
457
602
|
|
|
603
|
+
/**
|
|
604
|
+
* Reads at once (`request.bytes()`, no stream) the body of a request received by `Bun.serve()`
|
|
605
|
+
* whose `content-length` is declared: Bun reads exactly that many bytes, so checking the declared
|
|
606
|
+
* length enforces `limit`. Failures are those of {@link readWebStream}: 413 above `limit`, 400 when
|
|
607
|
+
* the client aborts.
|
|
608
|
+
*/
|
|
609
|
+
async function readFramedBody(request: Request, limit: number, contentLength: string | undefined): Promise<Uint8Array> {
|
|
610
|
+
if (exceedsDeclaredLength(contentLength, limit)) throw payloadTooLarge(limit)
|
|
611
|
+
let bytes: Uint8Array
|
|
612
|
+
try {
|
|
613
|
+
bytes = await request.bytes()
|
|
614
|
+
} catch (error) {
|
|
615
|
+
throw new BadRequestException('Failed to read the request body', undefined, { cause: error })
|
|
616
|
+
}
|
|
617
|
+
if (bytes.byteLength > limit) throw payloadTooLarge(limit)
|
|
618
|
+
return bytes
|
|
619
|
+
}
|
|
620
|
+
|
|
458
621
|
/**
|
|
459
622
|
* The headers of a Node.js request when none is repeated: then they hold the values of its Fetch
|
|
460
623
|
* `Headers` (Node.js keeps the first of some repeated headers where `Headers` join them all).
|
|
@@ -548,7 +711,11 @@ function toResponse(c: Context, reply: AdapterReply, middleware: boolean): Respo
|
|
|
548
711
|
|
|
549
712
|
/** Whether every header has a single value (a record of strings is a valid `HeadersInit`). */
|
|
550
713
|
function isStringRecord(headers: Readonly<Record<string, HeaderValue>>): boolean {
|
|
551
|
-
|
|
714
|
+
// Object.keys: faster than for...in on the null-prototype records of the pipeline (V8)
|
|
715
|
+
const names = Object.keys(headers)
|
|
716
|
+
for (let index = 0; index < names.length; index++) {
|
|
717
|
+
if (typeof headers[names[index] as string] !== 'string') return false
|
|
718
|
+
}
|
|
552
719
|
return true
|
|
553
720
|
}
|
|
554
721
|
|