@znight/http 0.0.0-stage → 0.1.1

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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2333 -2
  3. package/dist/adapter-CGSceVEN.d.mts +876 -0
  4. package/dist/compiler.d.mts +156 -0
  5. package/dist/compiler.mjs +2180 -0
  6. package/dist/compiler.mjs.map +1 -0
  7. package/dist/express.d.mts +39 -0
  8. package/dist/express.mjs +135 -0
  9. package/dist/express.mjs.map +1 -0
  10. package/dist/fastify.d.mts +33 -0
  11. package/dist/fastify.mjs +191 -0
  12. package/dist/fastify.mjs.map +1 -0
  13. package/dist/hono.d.mts +49 -0
  14. package/dist/hono.mjs +319 -0
  15. package/dist/hono.mjs.map +1 -0
  16. package/dist/index.d.mts +1254 -0
  17. package/dist/index.mjs +5618 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/node-BUnkdSsa.mjs +183 -0
  20. package/dist/node-BUnkdSsa.mjs.map +1 -0
  21. package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
  22. package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
  23. package/dist/node.d.mts +30 -0
  24. package/dist/node.mjs +1 -0
  25. package/dist/path-JI87C4k8.d.mts +109 -0
  26. package/dist/path.d.mts +2 -0
  27. package/dist/path.mjs +245 -0
  28. package/dist/path.mjs.map +1 -0
  29. package/dist/size-D8rsQcV0.mjs +184 -0
  30. package/dist/size-D8rsQcV0.mjs.map +1 -0
  31. package/dist/utils-DCbu3a4w.mjs +27 -0
  32. package/dist/utils-DCbu3a4w.mjs.map +1 -0
  33. package/package.json +142 -4
  34. package/src/adapter.ts +240 -0
  35. package/src/adapters/express.ts +201 -0
  36. package/src/adapters/fastify.ts +280 -0
  37. package/src/adapters/hono.ts +488 -0
  38. package/src/adapters/node-io.ts +368 -0
  39. package/src/adapters/node.ts +135 -0
  40. package/src/adapters/shared.ts +508 -0
  41. package/src/body.ts +135 -0
  42. package/src/coerce.ts +195 -0
  43. package/src/compiler/analyze.ts +1513 -0
  44. package/src/compiler/ast.ts +90 -0
  45. package/src/compiler/coercion.ts +267 -0
  46. package/src/compiler/generate.ts +374 -0
  47. package/src/compiler/index.ts +76 -0
  48. package/src/compiler/model.ts +260 -0
  49. package/src/compiler/route-decorators.ts +405 -0
  50. package/src/compiler/validate.ts +288 -0
  51. package/src/context.ts +533 -0
  52. package/src/cookie-settings.ts +68 -0
  53. package/src/cookie-signer.ts +106 -0
  54. package/src/cookies.ts +726 -0
  55. package/src/cors.ts +349 -0
  56. package/src/decisions.ts +331 -0
  57. package/src/decorators.ts +486 -0
  58. package/src/event-channel.ts +197 -0
  59. package/src/exceptions.ts +257 -0
  60. package/src/files.ts +429 -0
  61. package/src/headers.ts +39 -0
  62. package/src/index.ts +185 -0
  63. package/src/inject-sse.ts +309 -0
  64. package/src/interfaces.ts +101 -0
  65. package/src/metadata.ts +162 -0
  66. package/src/mime.ts +157 -0
  67. package/src/multipart.ts +597 -0
  68. package/src/option-error.ts +31 -0
  69. package/src/path.ts +288 -0
  70. package/src/pipeline.ts +918 -0
  71. package/src/plugin-routes.ts +188 -0
  72. package/src/plugin.ts +1432 -0
  73. package/src/proxy.ts +236 -0
  74. package/src/query.ts +30 -0
  75. package/src/response.ts +240 -0
  76. package/src/router.ts +119 -0
  77. package/src/security-headers.ts +161 -0
  78. package/src/serialize.ts +264 -0
  79. package/src/size.ts +66 -0
  80. package/src/sse-parse.ts +194 -0
  81. package/src/sse.ts +650 -0
  82. package/src/standard-schema.ts +81 -0
  83. package/src/status.ts +78 -0
  84. package/src/stream.ts +200 -0
  85. package/src/tokens.ts +21 -0
  86. package/src/uploads.ts +176 -0
  87. package/src/utils.ts +31 -0
  88. 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
+ }