@znight/http 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2333 -2
- package/dist/adapter-CGSceVEN.d.mts +876 -0
- package/dist/compiler.d.mts +156 -0
- package/dist/compiler.mjs +2180 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/express.d.mts +39 -0
- package/dist/express.mjs +135 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +33 -0
- package/dist/fastify.mjs +191 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +49 -0
- package/dist/hono.mjs +319 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +1254 -0
- package/dist/index.mjs +5618 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node-BUnkdSsa.mjs +183 -0
- package/dist/node-BUnkdSsa.mjs.map +1 -0
- package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
- package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
- package/dist/node.d.mts +30 -0
- package/dist/node.mjs +1 -0
- package/dist/path-JI87C4k8.d.mts +109 -0
- package/dist/path.d.mts +2 -0
- package/dist/path.mjs +245 -0
- package/dist/path.mjs.map +1 -0
- package/dist/size-D8rsQcV0.mjs +184 -0
- package/dist/size-D8rsQcV0.mjs.map +1 -0
- package/dist/utils-DCbu3a4w.mjs +27 -0
- package/dist/utils-DCbu3a4w.mjs.map +1 -0
- package/package.json +142 -4
- package/src/adapter.ts +240 -0
- package/src/adapters/express.ts +201 -0
- package/src/adapters/fastify.ts +280 -0
- package/src/adapters/hono.ts +488 -0
- package/src/adapters/node-io.ts +368 -0
- package/src/adapters/node.ts +135 -0
- package/src/adapters/shared.ts +508 -0
- package/src/body.ts +135 -0
- package/src/coerce.ts +195 -0
- package/src/compiler/analyze.ts +1513 -0
- package/src/compiler/ast.ts +90 -0
- package/src/compiler/coercion.ts +267 -0
- package/src/compiler/generate.ts +374 -0
- package/src/compiler/index.ts +76 -0
- package/src/compiler/model.ts +260 -0
- package/src/compiler/route-decorators.ts +405 -0
- package/src/compiler/validate.ts +288 -0
- package/src/context.ts +533 -0
- package/src/cookie-settings.ts +68 -0
- package/src/cookie-signer.ts +106 -0
- package/src/cookies.ts +726 -0
- package/src/cors.ts +349 -0
- package/src/decisions.ts +331 -0
- package/src/decorators.ts +486 -0
- package/src/event-channel.ts +197 -0
- package/src/exceptions.ts +257 -0
- package/src/files.ts +429 -0
- package/src/headers.ts +39 -0
- package/src/index.ts +185 -0
- package/src/inject-sse.ts +309 -0
- package/src/interfaces.ts +101 -0
- package/src/metadata.ts +162 -0
- package/src/mime.ts +157 -0
- package/src/multipart.ts +597 -0
- package/src/option-error.ts +31 -0
- package/src/path.ts +288 -0
- package/src/pipeline.ts +918 -0
- package/src/plugin-routes.ts +188 -0
- package/src/plugin.ts +1432 -0
- package/src/proxy.ts +236 -0
- package/src/query.ts +30 -0
- package/src/response.ts +240 -0
- package/src/router.ts +119 -0
- package/src/security-headers.ts +161 -0
- package/src/serialize.ts +264 -0
- package/src/size.ts +66 -0
- package/src/sse-parse.ts +194 -0
- package/src/sse.ts +650 -0
- package/src/standard-schema.ts +81 -0
- package/src/status.ts +78 -0
- package/src/stream.ts +200 -0
- package/src/tokens.ts +21 -0
- package/src/uploads.ts +176 -0
- package/src/utils.ts +31 -0
- package/src/validate.ts +65 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@znight/http/hono`: serves Znight routes with [Hono](https://hono.dev) 4 (optional peer
|
|
3
|
+
* dependency `hono`). On Bun the server is `Bun.serve()`; on Node.js it is `@hono/node-server`
|
|
4
|
+
* (optional peer dependency, loaded on `listen()`). `inject()` uses `app.request()`.
|
|
5
|
+
*/
|
|
6
|
+
import type { Server, ServerResponse } from 'node:http'
|
|
7
|
+
import type {
|
|
8
|
+
AdapterCloseOptions,
|
|
9
|
+
AdapterHandler,
|
|
10
|
+
AdapterReply,
|
|
11
|
+
AdapterRequest,
|
|
12
|
+
AdapterRoute,
|
|
13
|
+
FallbackHandler,
|
|
14
|
+
HttpAdapter,
|
|
15
|
+
HttpServerAddress,
|
|
16
|
+
InjectRequest,
|
|
17
|
+
InjectResponse,
|
|
18
|
+
InjectStreamRequest,
|
|
19
|
+
InjectStreamResponse,
|
|
20
|
+
} from '../adapter.ts'
|
|
21
|
+
|
|
22
|
+
import { type Context, Hono, type Next } from 'hono'
|
|
23
|
+
|
|
24
|
+
import { toHonoPath } from '../path.ts'
|
|
25
|
+
import { discardStream } from '../stream.ts'
|
|
26
|
+
import { isBun } from '../utils.ts'
|
|
27
|
+
import { ConnectionTracker, closeServer, onResponseAbort } from './node-io.ts'
|
|
28
|
+
import { bufferInjectStream, injectFetchStream, readWebStream, serverAddress, webBodyStream } from './shared.ts'
|
|
29
|
+
|
|
30
|
+
/** Options of {@link honoAdapter}. */
|
|
31
|
+
export interface HonoAdapterOptions {
|
|
32
|
+
/**
|
|
33
|
+
* An existing application: its middleware (`app.use(...)`) runs before the Znight routes. Znight
|
|
34
|
+
* sets its `notFound` and `onError` handlers.
|
|
35
|
+
*/
|
|
36
|
+
readonly app?: Hono
|
|
37
|
+
/**
|
|
38
|
+
* Options of `Bun.serve()` on Bun (`tls`, `idleTimeout`, `development`, `error`...); Znight sets
|
|
39
|
+
* `port`, `hostname` and `fetch`. Ignored on Node.js.
|
|
40
|
+
*/
|
|
41
|
+
readonly serveOptions?: HonoServeOptions
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Options of `Bun.serve()` (see the Bun documentation; Bun types are not required). Defaults set by
|
|
46
|
+
* the adapter: `maxRequestBodySize` unlimited (Znight enforces `bodyLimit` / `@BodyLimit` itself)
|
|
47
|
+
* and Bun's `idleTimeout` (10 s), lifted while a streamed response is sent.
|
|
48
|
+
*/
|
|
49
|
+
export interface HonoServeOptions {
|
|
50
|
+
/** Seconds a connection may stay idle (`0`: never closed). Default: 10. */
|
|
51
|
+
readonly idleTimeout?: number
|
|
52
|
+
/** Request bodies above this size are refused by Bun (413). Default: unlimited. */
|
|
53
|
+
readonly maxRequestBodySize?: number
|
|
54
|
+
/** TLS options (`{ key, cert }`). */
|
|
55
|
+
readonly tls?: unknown
|
|
56
|
+
/** Any other `Bun.serve()` option. */
|
|
57
|
+
readonly [option: string]: unknown
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The Hono adapter. */
|
|
61
|
+
export interface HonoAdapter extends HttpAdapter {
|
|
62
|
+
readonly name: 'hono'
|
|
63
|
+
/** The Hono application. */
|
|
64
|
+
readonly app: Hono
|
|
65
|
+
/** Fetch handler of the application (`export default { fetch: adapter.fetch }` on edge runtimes). */
|
|
66
|
+
readonly fetch: (request: Request, env?: unknown) => Response | Promise<Response>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates a Hono adapter.
|
|
71
|
+
*
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { honoAdapter } from '@znight/http/hono'
|
|
74
|
+
*
|
|
75
|
+
* HttpPlugin.configure({ adapter: honoAdapter() })
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function honoAdapter(options: HonoAdapterOptions = {}): HonoAdapter {
|
|
79
|
+
return new HonoHttpAdapter(options)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The parts of a Bun server used here (Bun types are not available to the package sources). */
|
|
83
|
+
interface BunServer {
|
|
84
|
+
readonly port: number
|
|
85
|
+
readonly hostname: string
|
|
86
|
+
stop(closeActiveConnections?: boolean): Promise<void> | void
|
|
87
|
+
requestIP?(request: Request): { readonly address: string } | null
|
|
88
|
+
/** Idle timeout of one request, in seconds (`0`: none). */
|
|
89
|
+
timeout?(request: Request, seconds: number): void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface BunRuntime {
|
|
93
|
+
serve(
|
|
94
|
+
options: HonoServeOptions & {
|
|
95
|
+
port: number
|
|
96
|
+
hostname: string
|
|
97
|
+
fetch: (request: Request, server: BunServer) => Response | Promise<Response>
|
|
98
|
+
},
|
|
99
|
+
): BunServer
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Default `idleTimeout` of `Bun.serve()`, in seconds. */
|
|
103
|
+
const BUN_IDLE_TIMEOUT = 10
|
|
104
|
+
|
|
105
|
+
/** A response body built while serving with `Bun.serve()` (see `#bunFetch`). */
|
|
106
|
+
interface BunBody {
|
|
107
|
+
/** The `Blob` (file) the body reads. */
|
|
108
|
+
readonly blob: Blob | undefined
|
|
109
|
+
/** Sets the function called when the body ends (read to its end, failed or cancelled). */
|
|
110
|
+
onEnd(callback: () => void): void
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Response bodies built while serving with `Bun.serve()`, by body (Hono may copy a `Response`, not its body). */
|
|
114
|
+
const BUN_BODIES = new WeakMap<ReadableStream<Uint8Array>, BunBody>()
|
|
115
|
+
|
|
116
|
+
/** The Node.js request behind a request of `@hono/node-server`. */
|
|
117
|
+
interface NodeIncoming {
|
|
118
|
+
readonly url?: string
|
|
119
|
+
readonly socket?: { readonly encrypted?: unknown }
|
|
120
|
+
readonly httpVersionMajor: number
|
|
121
|
+
readonly stream?: { readonly endAfterHeaders?: boolean }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Bindings of injected requests (the remote address the other adapters report for `inject()`). */
|
|
125
|
+
const INJECT_ENV = Object.freeze({ incoming: Object.freeze({ socket: Object.freeze({ remoteAddress: '127.0.0.1' }) }) })
|
|
126
|
+
|
|
127
|
+
type HonoMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'
|
|
128
|
+
|
|
129
|
+
class HonoHttpAdapter implements HonoAdapter {
|
|
130
|
+
readonly name = 'hono' as const
|
|
131
|
+
readonly app: Hono
|
|
132
|
+
readonly #serveOptions: HonoServeOptions
|
|
133
|
+
#fallback: FallbackHandler | undefined
|
|
134
|
+
#prepared = false
|
|
135
|
+
#bun: BunServer | undefined
|
|
136
|
+
#node: Server | undefined
|
|
137
|
+
#tracker: ConnectionTracker | undefined
|
|
138
|
+
#closing: Promise<void> | undefined
|
|
139
|
+
|
|
140
|
+
constructor(options: HonoAdapterOptions) {
|
|
141
|
+
this.app = options.app ?? new Hono()
|
|
142
|
+
this.#serveOptions = options.serveOptions ?? {}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
readonly fetch = (request: Request, env?: unknown): Response | Promise<Response> => {
|
|
146
|
+
this.#prepare()
|
|
147
|
+
return this.app.fetch(request, env)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
registerRoute(route: AdapterRoute): void {
|
|
151
|
+
const handler = route.handler
|
|
152
|
+
const path = toHonoPath(route.path)
|
|
153
|
+
if (route.method === 'HEAD') {
|
|
154
|
+
// Hono routes HEAD requests to GET handlers: serve them from a GET handler that only answers HEAD
|
|
155
|
+
this.app.get(path, (c: Context, next: Next) =>
|
|
156
|
+
c.req.raw.method === 'HEAD' ? this.#serve(c, handler) : next().then(() => c.res),
|
|
157
|
+
)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
this.app.on(route.method as HonoMethod, path, (c: Context) => this.#serve(c, handler))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setFallbackHandler(handler: FallbackHandler): void {
|
|
164
|
+
this.#fallback = handler
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#prepare(): void {
|
|
168
|
+
if (this.#prepared) return
|
|
169
|
+
this.#prepared = true
|
|
170
|
+
this.app.notFound(c => this.#serveFallback(c, undefined))
|
|
171
|
+
this.app.onError((error, c) => this.#serveFallback(c, error))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async #serve(c: Context, handler: AdapterHandler): Promise<Response> {
|
|
175
|
+
const request = adapterRequest(c)
|
|
176
|
+
// 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 path, like on the other adapters
|
|
178
|
+
const reply =
|
|
179
|
+
this.#fallback !== undefined && pathOf(request.url) !== pathOf(pathAndQuery(c.req.raw.url))
|
|
180
|
+
? await this.#fallback(request)
|
|
181
|
+
: await handler(request)
|
|
182
|
+
return this.#toResponse(c, reply)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async #serveFallback(c: Context, error: unknown): Promise<Response> {
|
|
186
|
+
if (this.#fallback === undefined) throw error ?? new Error('No fallback handler')
|
|
187
|
+
return this.#toResponse(c, await this.#fallback(adapterRequest(c), error))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#toResponse(c: Context, reply: AdapterReply): Response {
|
|
191
|
+
const outgoing = nodeOutgoing(c)
|
|
192
|
+
if (outgoing !== undefined) {
|
|
193
|
+
const body = reply.body instanceof ReadableStream ? reply.body : null
|
|
194
|
+
if (outgoing.destroyed) {
|
|
195
|
+
// @hono/node-server: the client left before the reply was ready (an async pre-flight). The
|
|
196
|
+
// server would neither read nor cancel the body: release it (event source, request scope)
|
|
197
|
+
discardStream(body)
|
|
198
|
+
return toResponse(c, { ...reply, body: null })
|
|
199
|
+
}
|
|
200
|
+
if (body !== null) {
|
|
201
|
+
// the client may leave before @hono/node-server reads the body (its own listener then comes
|
|
202
|
+
// too late): a body nobody locked is cancelled when the response closes unfinished
|
|
203
|
+
outgoing.once('close', () => {
|
|
204
|
+
if (!outgoing.writableFinished) discardStream(body)
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
// a quiet event stream must outlive a socket timeout
|
|
208
|
+
if (reply.eventStream === true && typeof outgoing.setTimeout === 'function') outgoing.setTimeout(0)
|
|
209
|
+
}
|
|
210
|
+
if (this.#bun === undefined || !(reply.body instanceof ReadableStream)) return toResponse(c, reply)
|
|
211
|
+
// served by Bun.serve(): observe the end of the body (idle timeout, native blob: see #bunFetch)
|
|
212
|
+
const reader = reply.body.getReader()
|
|
213
|
+
let ended = false
|
|
214
|
+
let onEnd = (): void => undefined
|
|
215
|
+
const end = (): void => {
|
|
216
|
+
if (ended) return
|
|
217
|
+
ended = true
|
|
218
|
+
onEnd()
|
|
219
|
+
}
|
|
220
|
+
const body = new ReadableStream<Uint8Array>(
|
|
221
|
+
{
|
|
222
|
+
async pull(controller) {
|
|
223
|
+
let result: Awaited<ReturnType<typeof reader.read>>
|
|
224
|
+
try {
|
|
225
|
+
result = await reader.read()
|
|
226
|
+
} catch (error) {
|
|
227
|
+
end()
|
|
228
|
+
throw error
|
|
229
|
+
}
|
|
230
|
+
if (result.done) {
|
|
231
|
+
end() // before closing: Bun ignores timeout() once the response is complete
|
|
232
|
+
controller.close()
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
controller.enqueue(result.value)
|
|
236
|
+
},
|
|
237
|
+
cancel(reason) {
|
|
238
|
+
end()
|
|
239
|
+
return reader.cancel(reason)
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{ highWaterMark: 0 },
|
|
243
|
+
)
|
|
244
|
+
BUN_BODIES.set(body, {
|
|
245
|
+
blob: reply.blob,
|
|
246
|
+
onEnd: callback => {
|
|
247
|
+
onEnd = callback
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
return toResponse(c, { ...reply, body })
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The fetch handler given to `Bun.serve()`. */
|
|
254
|
+
async #bunFetch(request: Request, server: BunServer, idleTimeout: number): Promise<Response> {
|
|
255
|
+
const response = await this.app.fetch(request, server)
|
|
256
|
+
const body = response.body
|
|
257
|
+
const served = body === null ? undefined : BUN_BODIES.get(body)
|
|
258
|
+
if (body === null || served === undefined) return response
|
|
259
|
+
BUN_BODIES.delete(body)
|
|
260
|
+
if (served.blob !== undefined && !body.locked) {
|
|
261
|
+
// a Blob is sent by Bun itself (sendfile, exact Content-Length; a stream would be chunked). Bun
|
|
262
|
+
// opens its file as soon as it takes the response: release the stream (and with it the request
|
|
263
|
+
// scope, which may delete the file) right after
|
|
264
|
+
const native = new Response(served.blob, {
|
|
265
|
+
status: response.status,
|
|
266
|
+
statusText: response.statusText,
|
|
267
|
+
headers: response.headers,
|
|
268
|
+
})
|
|
269
|
+
setImmediate(() => discardStream(body))
|
|
270
|
+
return native
|
|
271
|
+
}
|
|
272
|
+
if (idleTimeout > 0 && typeof server.timeout === 'function') {
|
|
273
|
+
// 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
|
|
275
|
+
server.timeout(request, 0)
|
|
276
|
+
served.onEnd(() => {
|
|
277
|
+
try {
|
|
278
|
+
server.timeout?.(request, idleTimeout)
|
|
279
|
+
} catch {
|
|
280
|
+
// the connection is gone
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
return response
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async listen(port: number, host: string): Promise<HttpServerAddress> {
|
|
288
|
+
this.#prepare()
|
|
289
|
+
this.#closing = undefined
|
|
290
|
+
const bun = (globalThis as { Bun?: BunRuntime }).Bun
|
|
291
|
+
if (isBun() && bun !== undefined) {
|
|
292
|
+
const options = this.#serveOptions
|
|
293
|
+
const idleTimeout = typeof options.idleTimeout === 'number' ? options.idleTimeout : BUN_IDLE_TIMEOUT
|
|
294
|
+
const server = bun.serve({
|
|
295
|
+
// Znight enforces the body limits (route @BodyLimit, plugin bodyLimit): not Bun's 128 MiB default
|
|
296
|
+
maxRequestBodySize: Number.MAX_SAFE_INTEGER,
|
|
297
|
+
...options,
|
|
298
|
+
port,
|
|
299
|
+
hostname: host,
|
|
300
|
+
fetch: (request, server) => this.#bunFetch(request, server, idleTimeout),
|
|
301
|
+
})
|
|
302
|
+
this.#bun = server
|
|
303
|
+
return serverAddress(server.hostname, server.port)
|
|
304
|
+
}
|
|
305
|
+
let nodeServer: typeof import('@hono/node-server')
|
|
306
|
+
try {
|
|
307
|
+
nodeServer = await import('@hono/node-server')
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
'honoAdapter on Node.js requires the optional peer dependency "@hono/node-server" (npm i @hono/node-server)',
|
|
311
|
+
{ cause: error },
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
const server = nodeServer.createAdaptorServer({ fetch: this.app.fetch }) as Server
|
|
315
|
+
this.#node = server
|
|
316
|
+
this.#tracker = new ConnectionTracker(server)
|
|
317
|
+
await new Promise<void>((resolve, reject) => {
|
|
318
|
+
const onError = (error: Error): void => reject(error)
|
|
319
|
+
server.once('error', onError)
|
|
320
|
+
server.listen(port, host, () => {
|
|
321
|
+
server.off('error', onError)
|
|
322
|
+
resolve()
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
const address = server.address()
|
|
326
|
+
if (address === null || typeof address === 'string') return serverAddress(host, port)
|
|
327
|
+
return serverAddress(address.address, address.port, address.family)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
close(options: AdapterCloseOptions = {}): Promise<void> {
|
|
331
|
+
const bun = this.#bun
|
|
332
|
+
if (bun !== undefined) {
|
|
333
|
+
if (options.force === true) {
|
|
334
|
+
this.#closing = Promise.resolve(bun.stop(true))
|
|
335
|
+
} else {
|
|
336
|
+
this.#closing ??= Promise.resolve(bun.stop(false))
|
|
337
|
+
}
|
|
338
|
+
return this.#closing
|
|
339
|
+
}
|
|
340
|
+
const server = this.#node
|
|
341
|
+
const tracker = this.#tracker
|
|
342
|
+
if (server === undefined || tracker === undefined) return Promise.resolve()
|
|
343
|
+
this.#closing = closeServer(server, tracker, this.#closing, options.force === true)
|
|
344
|
+
return this.#closing
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
inject(request: InjectRequest): Promise<InjectResponse> {
|
|
348
|
+
return bufferInjectStream(stream => this.injectStream(stream), request)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
injectStream(request: InjectStreamRequest): Promise<InjectStreamResponse> {
|
|
352
|
+
this.#prepare()
|
|
353
|
+
return injectFetchStream(request => this.app.request(request, undefined, INJECT_ENV), request)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function adapterRequest(c: Context): AdapterRequest {
|
|
358
|
+
const request = c.req.raw
|
|
359
|
+
const headers: Record<string, string> = {}
|
|
360
|
+
for (const [name, value] of request.headers) headers[name] = value
|
|
361
|
+
const incoming = nodeIncoming(c)
|
|
362
|
+
const address = remoteAddress(c)
|
|
363
|
+
let body: Promise<Uint8Array> | undefined
|
|
364
|
+
return {
|
|
365
|
+
method: request.method,
|
|
366
|
+
// the raw target on Node.js; on Bun only the normalised URL is known
|
|
367
|
+
url: pathAndQuery(typeof incoming?.url === 'string' && incoming.url !== '' ? incoming.url : request.url),
|
|
368
|
+
headers,
|
|
369
|
+
ip: address,
|
|
370
|
+
remoteAddress: address,
|
|
371
|
+
encrypted: incoming === undefined ? request.url.startsWith('https:') : incoming.socket?.encrypted === true,
|
|
372
|
+
raw: request,
|
|
373
|
+
rawResponse: c,
|
|
374
|
+
readBody: (limit: number): Promise<Uint8Array> => {
|
|
375
|
+
body ??= readWebStream(() => request.body, limit, headers['content-length'])
|
|
376
|
+
return body
|
|
377
|
+
},
|
|
378
|
+
streamBody: (limit: number): ReadableStream<Uint8Array> =>
|
|
379
|
+
webBodyStream(() => request.body, limit, headers['content-length']),
|
|
380
|
+
// asked only without content-length / transfer-encoding: a request read from an HTTP/1.1
|
|
381
|
+
// connection then has no body (`@hono/node-server` still gives every POST a body stream), an
|
|
382
|
+
// HTTP/2 one unless its HEADERS frame ended the stream; a Web `Request` (Bun, inject) says it
|
|
383
|
+
hasBody: (): boolean =>
|
|
384
|
+
incoming === undefined
|
|
385
|
+
? request.body !== null
|
|
386
|
+
: incoming.httpVersionMajor >= 2 && incoming.stream?.endAfterHeaders === false,
|
|
387
|
+
onAbort: (listener: () => void): void => {
|
|
388
|
+
const outgoing = nodeOutgoing(c)
|
|
389
|
+
if (outgoing !== undefined) {
|
|
390
|
+
onResponseAbort(outgoing as unknown as ServerResponse, listener)
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
// Bun.serve() and inject(): the request signal aborts when the client leaves
|
|
394
|
+
const signal = request.signal
|
|
395
|
+
if (signal.aborted) queueMicrotask(listener)
|
|
396
|
+
else signal.addEventListener('abort', listener, { once: true })
|
|
397
|
+
},
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The part of a Node.js `ServerResponse` used here. */
|
|
402
|
+
interface NodeOutgoing {
|
|
403
|
+
readonly writableFinished: boolean
|
|
404
|
+
readonly destroyed: boolean
|
|
405
|
+
once(event: 'close', listener: () => void): unknown
|
|
406
|
+
setTimeout?(ms: number): unknown
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** The Node.js response of `@hono/node-server` (`c.env.outgoing`), absent on Bun and for `inject()`. */
|
|
410
|
+
function nodeOutgoing(c: Context): NodeOutgoing | undefined {
|
|
411
|
+
const outgoing = (c.env as { outgoing?: Partial<NodeOutgoing> } | undefined)?.outgoing
|
|
412
|
+
return typeof outgoing?.once === 'function' ? (outgoing as NodeOutgoing) : undefined
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** The Node.js request of `@hono/node-server` (`c.env.incoming`), absent on Bun and for `inject()`. */
|
|
416
|
+
function nodeIncoming(c: Context): NodeIncoming | undefined {
|
|
417
|
+
const incoming = (c.env as { incoming?: Partial<NodeIncoming> } | undefined)?.incoming
|
|
418
|
+
return typeof incoming?.httpVersionMajor === 'number' ? (incoming as NodeIncoming) : undefined
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function pathAndQuery(url: string): string {
|
|
422
|
+
if (url.startsWith('/')) return url
|
|
423
|
+
const scheme = url.indexOf('://')
|
|
424
|
+
const start = url.indexOf('/', scheme === -1 ? 0 : scheme + 3)
|
|
425
|
+
return start === -1 ? '/' : url.slice(start)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function pathOf(url: string): string {
|
|
429
|
+
const query = url.indexOf('?')
|
|
430
|
+
return query === -1 ? url : url.slice(0, query)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function remoteAddress(c: Context): string | undefined {
|
|
434
|
+
const env = c.env as
|
|
435
|
+
| { incoming?: { socket?: { remoteAddress?: string } }; requestIP?: BunServer['requestIP'] }
|
|
436
|
+
| undefined
|
|
437
|
+
if (env === undefined || env === null) return undefined
|
|
438
|
+
if (typeof env.requestIP === 'function') return env.requestIP(c.req.raw)?.address ?? undefined
|
|
439
|
+
return env.incoming?.socket?.remoteAddress
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function toResponse(c: Context, reply: AdapterReply): Response {
|
|
443
|
+
if (reply.raw === true) {
|
|
444
|
+
discardStream(reply.body instanceof ReadableStream ? reply.body : null)
|
|
445
|
+
return c.res
|
|
446
|
+
}
|
|
447
|
+
const headers = new Headers()
|
|
448
|
+
try {
|
|
449
|
+
for (const name of Object.keys(reply.headers)) {
|
|
450
|
+
const value = reply.headers[name]
|
|
451
|
+
if (typeof value === 'string') headers.set(name, value)
|
|
452
|
+
else if (value !== undefined) for (const item of value) headers.append(name, item)
|
|
453
|
+
}
|
|
454
|
+
} catch {
|
|
455
|
+
// a header Headers refuses (the plugin validates them): never rethrow into Hono's onError, which
|
|
456
|
+
// would run the pipeline again; release the body and answer a plain 500, like writeNodeReply
|
|
457
|
+
discardStream(reply.body instanceof ReadableStream ? reply.body : null)
|
|
458
|
+
return new Response('{"statusCode":500,"error":"Internal Server Error"}', {
|
|
459
|
+
status: 500,
|
|
460
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
keepContextCookies(c, headers)
|
|
464
|
+
return new Response(reply.body, { status: reply.status, headers })
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Keeps the cookies Hono middleware set before the Znight route (`setCookie(c, ...)`,
|
|
469
|
+
* `c.header('set-cookie', ..., { append: true })`) before the reply's own, like the other adapters.
|
|
470
|
+
* Hono drops the headers of the context when a handler returns its own `Response` (or, when a
|
|
471
|
+
* middleware read `c.res`, replaces the response's cookies with the context's): the reply's cookies
|
|
472
|
+
* are added to the context too, so that both ways end with every cookie, in order.
|
|
473
|
+
*/
|
|
474
|
+
function keepContextCookies(c: Context, headers: Headers): void {
|
|
475
|
+
let context: string[]
|
|
476
|
+
try {
|
|
477
|
+
// the headers of the context without changing it (`c.res` would create the context response)
|
|
478
|
+
context = c.newResponse(null).headers.getSetCookie()
|
|
479
|
+
} catch {
|
|
480
|
+
return // a status a bodyless Response refuses (`c.status(101)`): nothing to keep
|
|
481
|
+
}
|
|
482
|
+
if (context.length === 0) return
|
|
483
|
+
const own = headers.getSetCookie()
|
|
484
|
+
for (const line of own) c.header('set-cookie', line, { append: true })
|
|
485
|
+
headers.delete('set-cookie')
|
|
486
|
+
for (const line of context) headers.append('set-cookie', line)
|
|
487
|
+
for (const line of own) headers.append('set-cookie', line)
|
|
488
|
+
}
|