@znight/http 0.1.1 → 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/LICENSE +1 -1
- package/README.md +83 -32
- package/dist/{adapter-CGSceVEN.d.mts → adapter-J0UXFnNM.d.mts} +7 -1
- package/dist/compiler.d.mts +1 -1
- package/dist/compiler.mjs +82 -13
- package/dist/compiler.mjs.map +1 -1
- package/dist/express.d.mts +1 -1
- package/dist/express.mjs +3 -2
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.d.mts +1 -1
- package/dist/fastify.mjs +254 -42
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +1 -1
- package/dist/hono.mjs +272 -67
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +10 -2
- package/dist/index.mjs +930 -278
- package/dist/index.mjs.map +1 -1
- package/dist/{node-BUnkdSsa.mjs → node-DnYv_AMI.mjs} +4 -3
- package/dist/{node-BUnkdSsa.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
- package/dist/{node-io-BY1ZDLRQ.mjs → node-io-BkdRSQeg.mjs} +151 -86
- package/dist/{node-io-BY1ZDLRQ.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
- package/dist/node.d.mts +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/adapter.ts +6 -0
- package/src/adapters/express.ts +4 -1
- package/src/adapters/fastify.ts +329 -44
- package/src/adapters/hono.ts +341 -86
- package/src/adapters/node-io.ts +185 -82
- 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 +151 -78
- package/src/index.ts +1 -1
- package/src/pipeline.ts +809 -208
- package/src/plugin.ts +10 -4
- package/src/query.ts +7 -1
- package/src/response.ts +6 -8
- package/src/serialize.ts +83 -16
- 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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* dependency `hono`). On Bun the server is `Bun.serve()`; on Node.js it is `@hono/node-server`
|
|
4
4
|
* (optional peer dependency, loaded on `listen()`). `inject()` uses `app.request()`.
|
|
5
5
|
*/
|
|
6
|
-
import type { Server, ServerResponse } from 'node:http'
|
|
6
|
+
import type { IncomingMessage, Server, ServerResponse } from 'node:http'
|
|
7
7
|
import type {
|
|
8
8
|
AdapterCloseOptions,
|
|
9
9
|
AdapterHandler,
|
|
@@ -18,14 +18,24 @@ import type {
|
|
|
18
18
|
InjectStreamRequest,
|
|
19
19
|
InjectStreamResponse,
|
|
20
20
|
} from '../adapter.ts'
|
|
21
|
+
import type { HeaderValue } from '../exceptions.ts'
|
|
21
22
|
|
|
22
23
|
import { type Context, Hono, type Next } from 'hono'
|
|
23
24
|
|
|
25
|
+
import { BadRequestException } from '../exceptions.ts'
|
|
24
26
|
import { toHonoPath } from '../path.ts'
|
|
25
27
|
import { discardStream } from '../stream.ts'
|
|
26
28
|
import { isBun } from '../utils.ts'
|
|
27
|
-
import { ConnectionTracker, closeServer, onResponseAbort } from './node-io.ts'
|
|
28
|
-
import {
|
|
29
|
+
import { ConnectionTracker, closeServer, nodeBodyStream, onResponseAbort, readNodeBody } from './node-io.ts'
|
|
30
|
+
import {
|
|
31
|
+
bufferInjectStream,
|
|
32
|
+
exceedsDeclaredLength,
|
|
33
|
+
injectFetchStream,
|
|
34
|
+
payloadTooLarge,
|
|
35
|
+
readWebStream,
|
|
36
|
+
serverAddress,
|
|
37
|
+
webBodyStream,
|
|
38
|
+
} from './shared.ts'
|
|
29
39
|
|
|
30
40
|
/** Options of {@link honoAdapter}. */
|
|
31
41
|
export interface HonoAdapterOptions {
|
|
@@ -102,12 +112,47 @@ interface BunRuntime {
|
|
|
102
112
|
/** Default `idleTimeout` of `Bun.serve()`, in seconds. */
|
|
103
113
|
const BUN_IDLE_TIMEOUT = 10
|
|
104
114
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
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 {
|
|
107
120
|
/** The `Blob` (file) the body reads. */
|
|
108
121
|
readonly blob: Blob | undefined
|
|
109
|
-
|
|
110
|
-
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
|
+
}
|
|
111
156
|
}
|
|
112
157
|
|
|
113
158
|
/** Response bodies built while serving with `Bun.serve()`, by body (Hono may copy a `Response`, not its body). */
|
|
@@ -126,13 +171,31 @@ const INJECT_ENV = Object.freeze({ incoming: Object.freeze({ socket: Object.free
|
|
|
126
171
|
|
|
127
172
|
type HonoMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'
|
|
128
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
|
+
|
|
129
180
|
class HonoHttpAdapter implements HonoAdapter {
|
|
130
181
|
readonly name = 'hono' as const
|
|
131
182
|
readonly app: Hono
|
|
132
183
|
readonly #serveOptions: HonoServeOptions
|
|
184
|
+
/** Number of routes of `app` registered by the adapter (see {@link #middleware}). */
|
|
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>()
|
|
133
188
|
#fallback: FallbackHandler | undefined
|
|
134
189
|
#prepared = false
|
|
135
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
|
|
136
199
|
#node: Server | undefined
|
|
137
200
|
#tracker: ConnectionTracker | undefined
|
|
138
201
|
#closing: Promise<void> | undefined
|
|
@@ -150,13 +213,29 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
150
213
|
registerRoute(route: AdapterRoute): void {
|
|
151
214
|
const handler = route.handler
|
|
152
215
|
const path = toHonoPath(route.path)
|
|
153
|
-
if (route.method === 'HEAD') {
|
|
154
|
-
// Hono routes HEAD requests to GET handlers:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)
|
|
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
|
+
})
|
|
158
236
|
return
|
|
159
237
|
}
|
|
238
|
+
this.#routes++
|
|
160
239
|
this.app.on(route.method as HonoMethod, path, (c: Context) => this.#serve(c, handler))
|
|
161
240
|
}
|
|
162
241
|
|
|
@@ -172,11 +251,12 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
172
251
|
}
|
|
173
252
|
|
|
174
253
|
async #serve(c: Context, handler: AdapterHandler): Promise<Response> {
|
|
175
|
-
const request =
|
|
254
|
+
const request = this.#request(c)
|
|
176
255
|
// Hono routes the WHATWG-normalised URL (`/a/../b` → `/b`, `%2e%2e` resolved): when the raw
|
|
177
|
-
// 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
|
|
178
258
|
const reply =
|
|
179
|
-
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))
|
|
180
260
|
? await this.#fallback(request)
|
|
181
261
|
: await handler(request)
|
|
182
262
|
return this.#toResponse(c, reply)
|
|
@@ -184,10 +264,33 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
184
264
|
|
|
185
265
|
async #serveFallback(c: Context, error: unknown): Promise<Response> {
|
|
186
266
|
if (this.#fallback === undefined) throw error ?? new Error('No fallback handler')
|
|
187
|
-
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
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Whether `app` has routes the adapter did not register (middleware, other routes): only they can
|
|
281
|
+
* set headers on the context or read the response.
|
|
282
|
+
*/
|
|
283
|
+
#middleware(): boolean {
|
|
284
|
+
return this.app.routes.length !== this.#routes
|
|
188
285
|
}
|
|
189
286
|
|
|
190
287
|
#toResponse(c: Context, reply: AdapterReply): Response {
|
|
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
|
+
}
|
|
191
294
|
const outgoing = nodeOutgoing(c)
|
|
192
295
|
if (outgoing !== undefined) {
|
|
193
296
|
const body = reply.body instanceof ReadableStream ? reply.body : null
|
|
@@ -195,7 +298,7 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
195
298
|
// @hono/node-server: the client left before the reply was ready (an async pre-flight). The
|
|
196
299
|
// server would neither read nor cancel the body: release it (event source, request scope)
|
|
197
300
|
discardStream(body)
|
|
198
|
-
return toResponse(c, { ...reply, body: null })
|
|
301
|
+
return toResponse(c, { ...reply, body: null }, middleware)
|
|
199
302
|
}
|
|
200
303
|
if (body !== null) {
|
|
201
304
|
// the client may leave before @hono/node-server reads the body (its own listener then comes
|
|
@@ -207,16 +310,23 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
207
310
|
// a quiet event stream must outlive a socket timeout
|
|
208
311
|
if (reply.eventStream === true && typeof outgoing.setTimeout === 'function') outgoing.setTimeout(0)
|
|
209
312
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
)
|
|
220
330
|
const body = new ReadableStream<Uint8Array>(
|
|
221
331
|
{
|
|
222
332
|
async pull(controller) {
|
|
@@ -224,39 +334,53 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
224
334
|
try {
|
|
225
335
|
result = await reader.read()
|
|
226
336
|
} catch (error) {
|
|
227
|
-
end()
|
|
337
|
+
served.end()
|
|
228
338
|
throw error
|
|
229
339
|
}
|
|
230
340
|
if (result.done) {
|
|
231
|
-
end() // before closing: Bun ignores timeout() once the response is complete
|
|
341
|
+
served.end() // before closing: Bun ignores timeout() once the response is complete
|
|
232
342
|
controller.close()
|
|
233
343
|
return
|
|
234
344
|
}
|
|
235
345
|
controller.enqueue(result.value)
|
|
236
346
|
},
|
|
237
347
|
cancel(reason) {
|
|
238
|
-
end()
|
|
348
|
+
served.end()
|
|
239
349
|
return reader.cancel(reason)
|
|
240
350
|
},
|
|
241
351
|
},
|
|
242
352
|
{ highWaterMark: 0 },
|
|
243
353
|
)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
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
|
|
251
364
|
}
|
|
252
365
|
|
|
253
|
-
/** The fetch handler given to `Bun.serve()
|
|
254
|
-
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> {
|
|
255
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
|
|
256
371
|
const body = response.body
|
|
257
372
|
const served = body === null ? undefined : BUN_BODIES.get(body)
|
|
258
373
|
if (body === null || served === undefined) return response
|
|
259
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 {
|
|
260
384
|
if (served.blob !== undefined && !body.locked) {
|
|
261
385
|
// a Blob is sent by Bun itself (sendfile, exact Content-Length; a stream would be chunked). Bun
|
|
262
386
|
// opens its file as soon as it takes the response: release the stream (and with it the request
|
|
@@ -269,9 +393,12 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
269
393
|
setImmediate(() => discardStream(body))
|
|
270
394
|
return native
|
|
271
395
|
}
|
|
272
|
-
|
|
396
|
+
const server = this.#bun
|
|
397
|
+
const idleTimeout = this.#idleTimeout
|
|
398
|
+
if (idleTimeout > 0 && !served.ended && typeof server?.timeout === 'function') {
|
|
273
399
|
// the idle timeout of Bun.serve() also cuts a response whose stream pauses (SSE, slow producers):
|
|
274
|
-
// 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)
|
|
275
402
|
server.timeout(request, 0)
|
|
276
403
|
served.onEnd(() => {
|
|
277
404
|
try {
|
|
@@ -290,14 +417,16 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
290
417
|
const bun = (globalThis as { Bun?: BunRuntime }).Bun
|
|
291
418
|
if (isBun() && bun !== undefined) {
|
|
292
419
|
const options = this.#serveOptions
|
|
293
|
-
|
|
420
|
+
this.#idleTimeout = typeof options.idleTimeout === 'number' ? options.idleTimeout : BUN_IDLE_TIMEOUT
|
|
294
421
|
const server = bun.serve({
|
|
295
422
|
// Znight enforces the body limits (route @BodyLimit, plugin bodyLimit): not Bun's 128 MiB default
|
|
296
423
|
maxRequestBodySize: Number.MAX_SAFE_INTEGER,
|
|
297
424
|
...options,
|
|
298
425
|
port,
|
|
299
426
|
hostname: host,
|
|
300
|
-
|
|
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),
|
|
301
430
|
})
|
|
302
431
|
this.#bun = server
|
|
303
432
|
return serverAddress(server.hostname, server.port)
|
|
@@ -354,48 +483,153 @@ class HonoHttpAdapter implements HonoAdapter {
|
|
|
354
483
|
}
|
|
355
484
|
}
|
|
356
485
|
|
|
357
|
-
|
|
358
|
-
|
|
486
|
+
/** A Hono request as an {@link AdapterRequest} (a class: no closure per request). */
|
|
487
|
+
class HonoAdapterRequest implements AdapterRequest {
|
|
488
|
+
readonly method: string
|
|
489
|
+
readonly url: string
|
|
490
|
+
readonly headers: Readonly<Record<string, string | undefined>>
|
|
491
|
+
readonly ip: string | undefined
|
|
492
|
+
readonly remoteAddress: string | undefined
|
|
493
|
+
readonly encrypted: boolean
|
|
494
|
+
readonly raw: Request
|
|
495
|
+
readonly rawResponse: Context
|
|
496
|
+
/** `url` is the raw request target (Node.js), not the WHATWG-normalised URL of `raw`. */
|
|
497
|
+
readonly rawTarget: boolean
|
|
498
|
+
/** The Node.js request of `@hono/node-server`, absent on Bun and for `inject()`. */
|
|
499
|
+
readonly #incoming: NodeIncoming | undefined
|
|
500
|
+
/**
|
|
501
|
+
* The same request over HTTP/1.1, read directly (Node.js streams): no Fetch `Request` and body
|
|
502
|
+
* stream are built for it.
|
|
503
|
+
*/
|
|
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
|
|
507
|
+
#body: Promise<Uint8Array> | undefined
|
|
508
|
+
|
|
509
|
+
constructor(c: Context, bun: BunServer | undefined) {
|
|
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
|
+
}
|
|
530
|
+
const incoming = nodeIncoming(c)
|
|
531
|
+
const http1 = incoming !== undefined && incoming.httpVersionMajor < 2 ? (incoming as IncomingMessage) : undefined
|
|
532
|
+
const address = remoteAddress(c)
|
|
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)
|
|
537
|
+
this.headers = (http1 === undefined ? undefined : distinctHeaders(http1)) ?? fetchHeaders(request.headers)
|
|
538
|
+
this.ip = address
|
|
539
|
+
this.remoteAddress = address
|
|
540
|
+
this.encrypted = incoming === undefined ? request.url.startsWith('https:') : incoming.socket?.encrypted === true
|
|
541
|
+
this.#incoming = incoming
|
|
542
|
+
this.#http1 = http1
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
readBody(limit: number): Promise<Uint8Array> {
|
|
546
|
+
if (this.#body === undefined) {
|
|
547
|
+
const http1 = this.#http1
|
|
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'])
|
|
551
|
+
}
|
|
552
|
+
return this.#body
|
|
553
|
+
}
|
|
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
|
+
|
|
564
|
+
streamBody(limit: number): ReadableStream<Uint8Array> {
|
|
565
|
+
const http1 = this.#http1
|
|
566
|
+
if (http1 !== undefined) return nodeBodyStream(http1, limit)
|
|
567
|
+
return webBodyStream(() => this.raw.body, limit, this.headers['content-length'])
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// asked only without content-length / transfer-encoding: a request read from an HTTP/1.1
|
|
571
|
+
// connection then has no body (`@hono/node-server` still gives every POST a body stream), an
|
|
572
|
+
// HTTP/2 one unless its HEADERS frame ended the stream; a Web `Request` (Bun, inject) says it
|
|
573
|
+
hasBody(): boolean {
|
|
574
|
+
const incoming = this.#incoming
|
|
575
|
+
return incoming === undefined
|
|
576
|
+
? this.raw.body !== null
|
|
577
|
+
: incoming.httpVersionMajor >= 2 && incoming.stream?.endAfterHeaders === false
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
onAbort(listener: () => void): void {
|
|
581
|
+
const outgoing = this.#bun ? undefined : nodeOutgoing(this.rawResponse)
|
|
582
|
+
if (outgoing !== undefined) {
|
|
583
|
+
onResponseAbort(outgoing as unknown as ServerResponse, listener)
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
// Bun.serve() and inject(): the request signal aborts when the client leaves
|
|
587
|
+
const signal = this.raw.signal
|
|
588
|
+
if (signal.aborted) queueMicrotask(listener)
|
|
589
|
+
else signal.addEventListener('abort', listener, { once: true })
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** The headers of a Fetch request: lower-case names, repeated headers joined (`Headers` semantics). */
|
|
594
|
+
function fetchHeaders(source: Headers): Record<string, string> {
|
|
359
595
|
const headers: Record<string, string> = {}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (signal.aborted) queueMicrotask(listener)
|
|
396
|
-
else signal.addEventListener('abort', listener, { once: true })
|
|
397
|
-
},
|
|
596
|
+
// forEach: no iterator nor entry array per header
|
|
597
|
+
source.forEach((value, name) => {
|
|
598
|
+
headers[name] = value
|
|
599
|
+
})
|
|
600
|
+
return headers
|
|
601
|
+
}
|
|
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
|
+
|
|
621
|
+
/**
|
|
622
|
+
* The headers of a Node.js request when none is repeated: then they hold the values of its Fetch
|
|
623
|
+
* `Headers` (Node.js keeps the first of some repeated headers where `Headers` join them all).
|
|
624
|
+
*/
|
|
625
|
+
function distinctHeaders(request: IncomingMessage): Record<string, string | undefined> | undefined {
|
|
626
|
+
const headers = request.headers
|
|
627
|
+
let count = 0
|
|
628
|
+
for (const name in headers) {
|
|
629
|
+
if (typeof headers[name] !== 'string') return undefined
|
|
630
|
+
count++
|
|
398
631
|
}
|
|
632
|
+
return count * 2 === request.rawHeaders.length ? (headers as Record<string, string | undefined>) : undefined
|
|
399
633
|
}
|
|
400
634
|
|
|
401
635
|
/** The part of a Node.js `ServerResponse` used here. */
|
|
@@ -439,11 +673,22 @@ function remoteAddress(c: Context): string | undefined {
|
|
|
439
673
|
return env.incoming?.socket?.remoteAddress
|
|
440
674
|
}
|
|
441
675
|
|
|
442
|
-
|
|
676
|
+
/**
|
|
677
|
+
* The `Response` of a reply. Without `middleware` (see `#middleware`), nothing set headers on the
|
|
678
|
+
* context and nothing reads the response: headers given as a record of strings are sent as is by
|
|
679
|
+
* `@hono/node-server` (no `Headers` object), and a text body as a string.
|
|
680
|
+
*/
|
|
681
|
+
function toResponse(c: Context, reply: AdapterReply, middleware: boolean): Response {
|
|
443
682
|
if (reply.raw === true) {
|
|
444
683
|
discardStream(reply.body instanceof ReadableStream ? reply.body : null)
|
|
445
684
|
return c.res
|
|
446
685
|
}
|
|
686
|
+
if (!middleware && isStringRecord(reply.headers)) {
|
|
687
|
+
return new Response(reply.text ?? reply.body, {
|
|
688
|
+
status: reply.status,
|
|
689
|
+
headers: reply.headers as Readonly<Record<string, string>>,
|
|
690
|
+
})
|
|
691
|
+
}
|
|
447
692
|
const headers = new Headers()
|
|
448
693
|
try {
|
|
449
694
|
for (const name of Object.keys(reply.headers)) {
|
|
@@ -460,8 +705,18 @@ function toResponse(c: Context, reply: AdapterReply): Response {
|
|
|
460
705
|
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
461
706
|
})
|
|
462
707
|
}
|
|
463
|
-
keepContextCookies(c, headers)
|
|
464
|
-
return new Response(reply.body, { status: reply.status, headers })
|
|
708
|
+
if (middleware) keepContextCookies(c, headers)
|
|
709
|
+
return new Response(reply.text ?? reply.body, { status: reply.status, headers })
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Whether every header has a single value (a record of strings is a valid `HeadersInit`). */
|
|
713
|
+
function isStringRecord(headers: Readonly<Record<string, HeaderValue>>): boolean {
|
|
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
|
+
}
|
|
719
|
+
return true
|
|
465
720
|
}
|
|
466
721
|
|
|
467
722
|
/**
|