@znight/http 0.1.0 → 0.1.2
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 +4 -0
- package/dist/{adapter-CGSceVEN.d.mts → adapter-J0UXFnNM.d.mts} +7 -1
- package/dist/compiler.d.mts +1 -1
- package/dist/express.d.mts +1 -1
- package/dist/express.mjs +1 -1
- package/dist/fastify.d.mts +1 -1
- package/dist/fastify.mjs +57 -22
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +1 -1
- package/dist/hono.mjs +112 -38
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +251 -157
- package/dist/index.mjs.map +1 -1
- package/dist/{node-BUnkdSsa.mjs → node-Clm1Ia7T.mjs} +2 -2
- package/dist/{node-BUnkdSsa.mjs.map → node-Clm1Ia7T.mjs.map} +1 -1
- package/dist/{node-io-BY1ZDLRQ.mjs → node-io-CVnqpVNS.mjs} +52 -26
- package/dist/{node-io-BY1ZDLRQ.mjs.map → node-io-CVnqpVNS.mjs.map} +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.mjs +1 -1
- package/package.json +4 -4
- package/src/adapter.ts +6 -0
- package/src/adapters/fastify.ts +66 -21
- package/src/adapters/hono.ts +135 -47
- package/src/adapters/node-io.ts +57 -24
- package/src/context.ts +7 -4
- package/src/pipeline.ts +237 -174
- package/src/plugin.ts +9 -2
- package/src/query.ts +7 -1
- package/src/serialize.ts +51 -11
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -2303,6 +2303,10 @@ interface HttpAdapter {
|
|
|
2303
2303
|
disconnects before the response was fully written (on the next microtask when that already
|
|
2304
2304
|
happened), never after a complete response. Without it, `ctx.signal` only aborts when the reply
|
|
2305
2305
|
body is cancelled or an event stream is closed by the server.
|
|
2306
|
+
- `AdapterReply.text` is the string a body was serialised from (JSON, text), next to its UTF-8
|
|
2307
|
+
bytes in `body`: an adapter that writes strings (`ServerResponse.end()`, `new Response()`) may
|
|
2308
|
+
send it instead, which writes the same bytes; on Node.js a string is written with the headers at
|
|
2309
|
+
once, where bytes take a second write.
|
|
2306
2310
|
- `AdapterReply.eventStream` marks a long-lived event stream (any streamed `text/event-stream`
|
|
2307
2311
|
body): send the headers without waiting for the first chunk, do not apply idle or inactivity
|
|
2308
2312
|
timeouts to the response while it is sent (`response.setTimeout(0)`, Bun's `server.timeout()`),
|
|
@@ -724,6 +724,12 @@ interface AdapterReply {
|
|
|
724
724
|
readonly headers: Readonly<Record<string, HeaderValue>>;
|
|
725
725
|
/** `null` for HEAD requests and bodyless statuses (headers such as `content-length` are kept). */
|
|
726
726
|
readonly body: ReplyBody;
|
|
727
|
+
/**
|
|
728
|
+
* The text `body` holds encoded as UTF-8, for a body serialised from a string (JSON, text).
|
|
729
|
+
* Adapters writing strings MAY send it instead of `body`, which writes the same bytes: Node.js then
|
|
730
|
+
* writes the headers and the body at once (bytes take a second write).
|
|
731
|
+
*/
|
|
732
|
+
readonly text?: string;
|
|
727
733
|
/**
|
|
728
734
|
* The `Blob` (file) that `body` streams, for adapters able to send it natively (`sendfile`, exact
|
|
729
735
|
* length: `new Response(blob)` on `Bun.serve()`). An adapter sending it instead of reading `body`
|
|
@@ -873,4 +879,4 @@ interface HttpAdapter {
|
|
|
873
879
|
injectStream?(request: InjectStreamRequest): Promise<InjectStreamResponse>;
|
|
874
880
|
}
|
|
875
881
|
export { PayloadTooLargeException as $, UploadedFileOptions as A, BadRequestException as B, HttpUploadDefinition as C, CookieOptions as Ct, RequestAbortedError as D, parseSetCookie as Dt, RequestAbortKind as E, parseCookieHeader as Et, formatByteSize as F, HeaderValue as G, ForbiddenException as H, parseByteSize as I, HttpExceptionOptions as J, HttpErrorBody as K, QueryRecord as L, Uploads as M, uploads as N, RequestId as O, serializeClearCookie as Ot, ByteSize as P, NotImplementedException as Q, parseQuery as R, HttpSseDefinition as S, CookieDefaults as St, HttpRouteInfo as T, SetCookieInfo as Tt, GatewayTimeoutException as U, ConflictException as V, GoneException as W, MethodNotAllowedException as X, InternalServerErrorException as Y, NotFoundException as Z, HttpMethod as _, StandardSchemaV1Result as _t, AdapterRoute as a, ValidationException as at, HttpRouteDefinition as b, isStandardSchema as bt, HttpServerAddress as c, toIssueDetail as ct, InjectStreamOptions as d, StandardSchemaV1 as dt, ServiceUnavailableException as et, InjectStreamRequest as f, StandardSchemaV1FailureResult as ft, HttpMetadata as g, StandardSchemaV1Props as gt, HttpControllerDefinition as h, StandardSchemaV1PathSegment as ht, AdapterRequest as i, UnsupportedMediaTypeException as it, UploadedFilesOptions as j, UploadLimits as k, serializeCookie as kt, InjectRequest as l, InferSchemaInput as lt, ReplyBody as m, StandardSchemaV1Options as mt, AdapterHandler as n, UnauthorizedException as nt, FallbackHandler as o, ValidationIssueDetail as ot, InjectStreamResponse as p, StandardSchemaV1Issue as pt, HttpException as q, AdapterReply as r, UnprocessableEntityException as rt, HttpAdapter as s, isHttpException as st, AdapterCloseOptions as t, TooManyRequestsException as tt, InjectResponse as u, InferSchemaOutput as ut, HttpParamDefinition as v, StandardSchemaV1SuccessResult as vt, HttpContext as w, ResponseCookie as wt, HttpRouteGroupDefinition as x, ClearCookieOptions as xt, HttpParamSource as y, StandardSchemaV1Types as yt, BadGatewayException as z };
|
|
876
|
-
//# sourceMappingURL=adapter-
|
|
882
|
+
//# sourceMappingURL=adapter-J0UXFnNM.d.mts.map
|
package/dist/compiler.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as HttpMethod, y as HttpParamSource } from "./adapter-
|
|
1
|
+
import { _ as HttpMethod, y as HttpParamSource } from "./adapter-J0UXFnNM.mjs";
|
|
2
2
|
import { ClassRef, CompilerExtension, Location, StaticValue, ValueRef } from "@znight/compiler";
|
|
3
3
|
/** Primitive coercions (`coerce.<kind>`). */
|
|
4
4
|
type ScalarCoercion = "string" | "number" | "integer" | "boolean" | "bigint" | "date";
|
package/dist/express.d.mts
CHANGED
package/dist/express.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as writeNodeReply, f as bufferInjectStream, i as nodeAdapterRequest, m as injectNodeStream, n as closeServer, t as ConnectionTracker, u as writeNodeFailure, y as serverAddress } from "./node-io-
|
|
1
|
+
import { d as writeNodeReply, f as bufferInjectStream, i as nodeAdapterRequest, m as injectNodeStream, n as closeServer, t as ConnectionTracker, u as writeNodeFailure, y as serverAddress } from "./node-io-CVnqpVNS.mjs";
|
|
2
2
|
import { toRegExpSource } from "./path.mjs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import express from "express";
|
package/dist/fastify.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { s as HttpAdapter } from "./adapter-
|
|
1
|
+
import { s as HttpAdapter } from "./adapter-J0UXFnNM.mjs";
|
|
2
2
|
import { FastifyInstance, FastifyServerOptions } from "fastify";
|
|
3
3
|
/** Options of {@link fastifyAdapter}. */
|
|
4
4
|
export interface FastifyAdapterOptions {
|
package/dist/fastify.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { N as discardStream, _ as lightStreamResponse, a as nodeBodyStream, c as readNodeBody, d as writeNodeReply, f as bufferInjectStream, g as lightInjectOptions, l as withNativeCookies, o as normalizeNodeHeaders, r as isEncrypted, s as onResponseAbort, t as ConnectionTracker, y as serverAddress } from "./node-io-
|
|
1
|
+
import { N as discardStream, _ as lightStreamResponse, a as nodeBodyStream, c as readNodeBody, d as writeNodeReply, f as bufferInjectStream, g as lightInjectOptions, l as withNativeCookies, o as normalizeNodeHeaders, r as isEncrypted, s as onResponseAbort, t as ConnectionTracker, y as serverAddress } from "./node-io-CVnqpVNS.mjs";
|
|
2
2
|
import { toFastifyPath } from "./path.mjs";
|
|
3
3
|
import { Readable } from "node:stream";
|
|
4
4
|
import Fastify from "fastify";
|
|
@@ -118,7 +118,10 @@ var FastifyHttpAdapter = class {
|
|
|
118
118
|
for (const name of Object.keys(result.headers)) reply.header(name, result.headers[name]);
|
|
119
119
|
const body = result.body;
|
|
120
120
|
if (body === null) return reply.send();
|
|
121
|
-
if (body instanceof Uint8Array)
|
|
121
|
+
if (body instanceof Uint8Array) {
|
|
122
|
+
if (result.text !== void 0 && sendsTextAsIs(result.headers["content-type"])) return reply.send(result.text);
|
|
123
|
+
return reply.send(Buffer.isBuffer(body) ? body : Buffer.from(body.buffer, body.byteOffset, body.byteLength));
|
|
124
|
+
}
|
|
122
125
|
const stream = body instanceof Blob ? body.stream() : body;
|
|
123
126
|
return reply.send(Readable.fromWeb(stream));
|
|
124
127
|
}
|
|
@@ -154,6 +157,18 @@ var FastifyHttpAdapter = class {
|
|
|
154
157
|
return lightStreamResponse(response, request.signal);
|
|
155
158
|
}
|
|
156
159
|
};
|
|
160
|
+
/** The JSON content type of the plugin (with a charset). */
|
|
161
|
+
const JSON_TYPE = "application/json; charset=utf-8";
|
|
162
|
+
/**
|
|
163
|
+
* Whether Fastify sends a string with this content type unchanged: it adds a charset to a JSON
|
|
164
|
+
* type without one, and a content type to a string without one.
|
|
165
|
+
*/
|
|
166
|
+
function sendsTextAsIs(type) {
|
|
167
|
+
if (typeof type !== "string") return false;
|
|
168
|
+
if (type === JSON_TYPE) return true;
|
|
169
|
+
const semicolon = type.indexOf(";");
|
|
170
|
+
return !(semicolon === -1 ? type : type.slice(0, semicolon)).toLowerCase().includes("json");
|
|
171
|
+
}
|
|
157
172
|
function lowerCaseHeaders(headers) {
|
|
158
173
|
const result = {};
|
|
159
174
|
for (const [name, value] of Object.entries(headers)) if (value !== void 0) result[name.toLowerCase()] = typeof value === "object" ? [...value] : String(value);
|
|
@@ -165,27 +180,47 @@ function adapterRequest(request, reply, response = reply) {
|
|
|
165
180
|
HIDDEN_CONTENT_TYPES.delete(request.raw);
|
|
166
181
|
request.raw.headers["content-type"] = hidden;
|
|
167
182
|
}
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
method: request.raw.method ?? request.method,
|
|
171
|
-
url: request.raw.url ?? request.url,
|
|
172
|
-
headers: normalizeNodeHeaders(request.headers),
|
|
173
|
-
ip: request.ip,
|
|
174
|
-
remoteAddress: request.raw.socket?.remoteAddress,
|
|
175
|
-
encrypted: isEncrypted(request.raw),
|
|
176
|
-
raw: request,
|
|
177
|
-
rawResponse: reply,
|
|
178
|
-
readBody: (limit) => {
|
|
179
|
-
body ??= readNodeBody(request.raw, limit);
|
|
180
|
-
return body;
|
|
181
|
-
},
|
|
182
|
-
streamBody: (limit) => nodeBodyStream(request.raw, limit),
|
|
183
|
-
rawStatus: () => reply?.statusCode,
|
|
184
|
-
onAbort: (listener) => {
|
|
185
|
-
if (response !== void 0) onResponseAbort(response.raw, listener);
|
|
186
|
-
}
|
|
187
|
-
};
|
|
183
|
+
return new FastifyAdapterRequest(request, reply, response);
|
|
188
184
|
}
|
|
185
|
+
/** A Fastify request as an {@link AdapterRequest} (a class: no closure per request). */
|
|
186
|
+
var FastifyAdapterRequest = class {
|
|
187
|
+
method;
|
|
188
|
+
url;
|
|
189
|
+
headers;
|
|
190
|
+
ip;
|
|
191
|
+
remoteAddress;
|
|
192
|
+
encrypted;
|
|
193
|
+
raw;
|
|
194
|
+
/** `@Res()`: the reply of a route, `undefined` for the fallback handler. */
|
|
195
|
+
rawResponse;
|
|
196
|
+
/** The reply whose disconnection aborts the request. */
|
|
197
|
+
#response;
|
|
198
|
+
#body;
|
|
199
|
+
constructor(request, reply, response) {
|
|
200
|
+
this.method = request.raw.method ?? request.method;
|
|
201
|
+
this.url = request.raw.url ?? request.url;
|
|
202
|
+
this.headers = normalizeNodeHeaders(request.headers);
|
|
203
|
+
this.ip = request.ip;
|
|
204
|
+
this.remoteAddress = request.raw.socket?.remoteAddress;
|
|
205
|
+
this.encrypted = isEncrypted(request.raw);
|
|
206
|
+
this.raw = request;
|
|
207
|
+
this.rawResponse = reply;
|
|
208
|
+
this.#response = response;
|
|
209
|
+
}
|
|
210
|
+
readBody(limit) {
|
|
211
|
+
this.#body ??= readNodeBody(this.raw.raw, limit);
|
|
212
|
+
return this.#body;
|
|
213
|
+
}
|
|
214
|
+
streamBody(limit) {
|
|
215
|
+
return nodeBodyStream(this.raw.raw, limit);
|
|
216
|
+
}
|
|
217
|
+
rawStatus() {
|
|
218
|
+
return this.rawResponse?.statusCode;
|
|
219
|
+
}
|
|
220
|
+
onAbort(listener) {
|
|
221
|
+
if (this.#response !== void 0) onResponseAbort(this.#response.raw, listener);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
189
224
|
export { fastifyAdapter };
|
|
190
225
|
|
|
191
226
|
//# sourceMappingURL=fastify.mjs.map
|
package/dist/fastify.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.mjs","names":[],"sources":["../src/adapters/fastify.ts"],"sourcesContent":["/**\n * `@znight/http/fastify`: serves Znight routes with [Fastify](https://fastify.dev) 5 (optional\n * peer dependency `fastify`). Routing uses Fastify's router; body parsing, validation and\n * serialisation stay Znight's, so responses are identical to the other adapters.\n */\nimport type {\n AdapterCloseOptions,\n AdapterHandler,\n AdapterReply,\n AdapterRequest,\n AdapterRoute,\n FallbackHandler,\n HttpAdapter,\n HttpServerAddress,\n InjectRequest,\n InjectResponse,\n InjectStreamRequest,\n InjectStreamResponse,\n} from '../adapter.ts'\n\nimport { Readable } from 'node:stream'\nimport Fastify, {\n type FastifyInstance,\n type FastifyReply,\n type FastifyRequest,\n type FastifyServerOptions,\n} from 'fastify'\n\nimport { toFastifyPath } from '../path.ts'\nimport { discardStream } from '../stream.ts'\nimport {\n ConnectionTracker,\n isEncrypted,\n nodeBodyStream,\n normalizeNodeHeaders,\n onResponseAbort,\n readNodeBody,\n withNativeCookies,\n writeNodeReply,\n} from './node-io.ts'\nimport {\n bufferInjectStream,\n type LightInjectOptions,\n lightInjectOptions,\n lightStreamResponse,\n serverAddress,\n} from './shared.ts'\n\n/** Options of {@link fastifyAdapter}. */\nexport interface FastifyAdapterOptions {\n /**\n * An existing instance (with your plugins and hooks). Znight registers its routes in an\n * encapsulated child context (its own body handling and 404 handler) and closes the instance on\n * shutdown. Malformed URLs are then answered by Fastify's `frameworkErrors` handler.\n */\n readonly instance?: FastifyInstance\n /**\n * Options of the instance created when `instance` is absent. Znight sets `exposeHeadRoutes: false`\n * (HEAD is served by Znight), a `maxParamLength` of 8 KiB and a `frameworkErrors` handler.\n */\n readonly options?: FastifyServerOptions\n}\n\n/** The Fastify adapter. */\nexport interface FastifyAdapter extends HttpAdapter {\n readonly name: 'fastify'\n /** The Fastify instance. */\n readonly instance: FastifyInstance\n}\n\n/**\n * Creates a Fastify adapter.\n *\n * ```ts\n * import { fastifyAdapter } from '@znight/http/fastify'\n *\n * HttpPlugin.configure({ adapter: fastifyAdapter({ options: { trustProxy: true } }) })\n * ```\n */\nexport function fastifyAdapter(options: FastifyAdapterOptions = {}): FastifyAdapter {\n return new FastifyHttpAdapter(options)\n}\n\nconst MAX_PARAM_LENGTH = 8192\n\n/** `content-type` headers hidden from Fastify (see `#register`), by request. */\nconst HIDDEN_CONTENT_TYPES = new WeakMap<object, string>()\n\nclass FastifyHttpAdapter implements FastifyAdapter {\n readonly name = 'fastify' as const\n readonly instance: FastifyInstance\n readonly #routes: AdapterRoute[] = []\n readonly #tracker: ConnectionTracker\n #fallback: FallbackHandler | undefined\n #ready: Promise<void> | undefined\n #closing: Promise<void> | undefined\n\n constructor(options: FastifyAdapterOptions) {\n if (options.instance === undefined) {\n const serverOptions = options.options ?? {}\n this.instance = Fastify({\n exposeHeadRoutes: false,\n frameworkErrors: (error, request, reply) => {\n this.#reply(reply, this.#dispatchFallback(request, reply, error)).catch(() => undefined)\n },\n ...serverOptions,\n routerOptions: { maxParamLength: MAX_PARAM_LENGTH, ...serverOptions.routerOptions },\n })\n } else {\n this.instance = options.instance\n }\n this.#tracker = new ConnectionTracker(this.instance.server)\n }\n\n registerRoute(route: AdapterRoute): void {\n if (this.#ready !== undefined) throw new Error('fastifyAdapter: routes must be registered before listen()/inject()')\n this.#routes.push(route)\n }\n\n setFallbackHandler(handler: FallbackHandler): void {\n this.#fallback = handler\n }\n\n #prepare(): Promise<void> {\n this.#ready ??= this.#register()\n return this.#ready\n }\n\n async #register(): Promise<void> {\n const routes = this.#routes\n await this.instance.register((child, _options, done) => {\n // Znight reads and parses bodies itself: accept every content type without consuming it\n child.removeAllContentTypeParsers()\n child.addContentTypeParser('*', (_request, _payload, done) => done(null, undefined))\n // Fastify answers 415 to a content type it cannot parse (`foo`, an empty one) before the route\n // runs: hide such a header from Fastify (Znight reads it, and parses the body as bytes)\n child.addHook('onRequest', (request, _reply, done) => {\n const contentType = request.headers['content-type']\n if (contentType !== undefined && request.mediaType === undefined) {\n HIDDEN_CONTENT_TYPES.set(request.raw, contentType)\n delete request.raw.headers['content-type']\n }\n done()\n })\n for (const route of routes) {\n const handler = route.handler\n child.route({\n method: route.method,\n url: toFastifyPath(route.path),\n exposeHeadRoute: false,\n handler: (request, reply) => this.#reply(reply, handler(adapterRequest(request, reply))),\n })\n }\n child.setNotFoundHandler((request, reply) =>\n this.#reply(reply, this.#dispatchFallback(request, reply, undefined)),\n )\n child.setErrorHandler((error, request, reply) =>\n this.#reply(reply, this.#dispatchFallback(request, reply, error)),\n )\n done()\n })\n await this.instance.ready()\n }\n\n #dispatchFallback(request: FastifyRequest, reply: FastifyReply, error: unknown): Promise<AdapterReply> {\n if (this.#fallback === undefined) return Promise.reject(error ?? new Error('No fallback handler'))\n return this.#fallback(adapterRequest(request, undefined, reply), error)\n }\n\n async #reply(reply: FastifyReply, pending: ReturnType<AdapterHandler>): Promise<FastifyReply> {\n let result: AdapterReply\n try {\n result = await pending\n } catch {\n result = {\n status: 500,\n headers: { 'content-type': 'application/json; charset=utf-8' },\n body: new TextEncoder().encode('{\"statusCode\":500,\"error\":\"Internal Server Error\"}'),\n }\n }\n if (result.raw === true || reply.sent || reply.raw.destroyed) {\n discardStream(result.body instanceof ReadableStream ? result.body : null)\n return reply\n }\n if (result.eventStream === true) {\n // no onSend hook (compression, ETag...) may rewrite an event stream: write it on the raw\n // response, with the headers Fastify hooks already set (`onRequest`...) under the plugin's,\n // and their cookies before the plugin's (as reply.header('set-cookie') does on the other path)\n const native = lowerCaseHeaders(reply.getHeaders())\n const headers = { ...native, ...withNativeCookies(native['set-cookie'], result.headers) }\n reply.hijack()\n void writeNodeReply(reply.raw, { ...result, headers })\n return reply\n }\n reply.code(result.status)\n for (const name of Object.keys(result.headers)) reply.header(name, result.headers[name])\n const body = result.body\n if (body === null) return reply.send()\n if (body instanceof Uint8Array) return reply.send(Buffer.from(body.buffer, body.byteOffset, body.byteLength))\n const stream = body instanceof Blob ? body.stream() : body\n return reply.send(Readable.fromWeb(stream as import('node:stream/web').ReadableStream))\n }\n\n async listen(port: number, host: string): Promise<HttpServerAddress> {\n await this.#prepare()\n this.#closing = undefined\n await this.instance.listen({ port, host })\n const address = this.instance.server.address()\n if (address === null || typeof address === 'string') return serverAddress(host, port)\n return serverAddress(address.address, address.port, address.family)\n }\n\n close(options: AdapterCloseOptions = {}): Promise<void> {\n if (this.#closing === undefined) {\n this.#tracker.closeIdle()\n this.#closing = this.instance.close()\n }\n if (options.force === true) {\n this.instance.server.closeAllConnections?.()\n this.#tracker.destroyAll()\n }\n return this.#closing\n }\n\n inject(request: InjectRequest): Promise<InjectResponse> {\n return bufferInjectStream(stream => this.injectStream(stream), request)\n }\n\n async injectStream(request: InjectStreamRequest): Promise<InjectStreamResponse> {\n await this.#prepare()\n const options: LightInjectOptions = await lightInjectOptions(request)\n // Fastify forwards the options to light-my-request (`payloadAsStream`)\n const inject = this.instance.inject as unknown as (options: LightInjectOptions) => Promise<unknown>\n const response = await inject.call(this.instance, options)\n return lightStreamResponse(response as Parameters<typeof lightStreamResponse>[0], request.signal)\n }\n}\n\nfunction lowerCaseHeaders(\n headers: Readonly<Record<string, string | number | readonly string[] | undefined>>,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {}\n for (const [name, value] of Object.entries(headers)) {\n if (value !== undefined) result[name.toLowerCase()] = typeof value === 'object' ? [...value] : String(value)\n }\n return result\n}\n\nfunction adapterRequest(\n request: FastifyRequest,\n reply: FastifyReply | undefined,\n response: FastifyReply | undefined = reply,\n): AdapterRequest {\n const hidden = HIDDEN_CONTENT_TYPES.get(request.raw)\n if (hidden !== undefined) {\n // Fastify is done with the content type: restore it (for Znight and `@Req()`)\n HIDDEN_CONTENT_TYPES.delete(request.raw)\n request.raw.headers['content-type'] = hidden\n }\n let body: Promise<Uint8Array> | undefined\n return {\n method: request.raw.method ?? request.method,\n url: request.raw.url ?? request.url,\n headers: normalizeNodeHeaders(request.headers),\n ip: request.ip,\n remoteAddress: request.raw.socket?.remoteAddress,\n encrypted: isEncrypted(request.raw),\n raw: request,\n rawResponse: reply,\n readBody: (limit: number): Promise<Uint8Array> => {\n body ??= readNodeBody(request.raw, limit)\n return body\n },\n streamBody: (limit: number): ReadableStream<Uint8Array> => nodeBodyStream(request.raw, limit),\n rawStatus: (): number | undefined => reply?.statusCode,\n onAbort: (listener: () => void): void => {\n if (response !== undefined) onResponseAbort(response.raw, listener)\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fastify.mjs","names":[],"sources":["../src/adapters/fastify.ts"],"sourcesContent":["/**\n * `@znight/http/fastify`: serves Znight routes with [Fastify](https://fastify.dev) 5 (optional\n * peer dependency `fastify`). Routing uses Fastify's router; body parsing, validation and\n * serialisation stay Znight's, so responses are identical to the other adapters.\n */\nimport type {\n AdapterCloseOptions,\n AdapterHandler,\n AdapterReply,\n AdapterRequest,\n AdapterRoute,\n FallbackHandler,\n HttpAdapter,\n HttpServerAddress,\n InjectRequest,\n InjectResponse,\n InjectStreamRequest,\n InjectStreamResponse,\n} from '../adapter.ts'\nimport type { HeaderValue } from '../exceptions.ts'\n\nimport { Readable } from 'node:stream'\nimport Fastify, {\n type FastifyInstance,\n type FastifyReply,\n type FastifyRequest,\n type FastifyServerOptions,\n} from 'fastify'\n\nimport { toFastifyPath } from '../path.ts'\nimport { discardStream } from '../stream.ts'\nimport {\n ConnectionTracker,\n isEncrypted,\n nodeBodyStream,\n normalizeNodeHeaders,\n onResponseAbort,\n readNodeBody,\n withNativeCookies,\n writeNodeReply,\n} from './node-io.ts'\nimport {\n bufferInjectStream,\n type LightInjectOptions,\n lightInjectOptions,\n lightStreamResponse,\n serverAddress,\n} from './shared.ts'\n\n/** Options of {@link fastifyAdapter}. */\nexport interface FastifyAdapterOptions {\n /**\n * An existing instance (with your plugins and hooks). Znight registers its routes in an\n * encapsulated child context (its own body handling and 404 handler) and closes the instance on\n * shutdown. Malformed URLs are then answered by Fastify's `frameworkErrors` handler.\n */\n readonly instance?: FastifyInstance\n /**\n * Options of the instance created when `instance` is absent. Znight sets `exposeHeadRoutes: false`\n * (HEAD is served by Znight), a `maxParamLength` of 8 KiB and a `frameworkErrors` handler.\n */\n readonly options?: FastifyServerOptions\n}\n\n/** The Fastify adapter. */\nexport interface FastifyAdapter extends HttpAdapter {\n readonly name: 'fastify'\n /** The Fastify instance. */\n readonly instance: FastifyInstance\n}\n\n/**\n * Creates a Fastify adapter.\n *\n * ```ts\n * import { fastifyAdapter } from '@znight/http/fastify'\n *\n * HttpPlugin.configure({ adapter: fastifyAdapter({ options: { trustProxy: true } }) })\n * ```\n */\nexport function fastifyAdapter(options: FastifyAdapterOptions = {}): FastifyAdapter {\n return new FastifyHttpAdapter(options)\n}\n\nconst MAX_PARAM_LENGTH = 8192\n\n/** `content-type` headers hidden from Fastify (see `#register`), by request. */\nconst HIDDEN_CONTENT_TYPES = new WeakMap<object, string>()\n\nclass FastifyHttpAdapter implements FastifyAdapter {\n readonly name = 'fastify' as const\n readonly instance: FastifyInstance\n readonly #routes: AdapterRoute[] = []\n readonly #tracker: ConnectionTracker\n #fallback: FallbackHandler | undefined\n #ready: Promise<void> | undefined\n #closing: Promise<void> | undefined\n\n constructor(options: FastifyAdapterOptions) {\n if (options.instance === undefined) {\n const serverOptions = options.options ?? {}\n this.instance = Fastify({\n exposeHeadRoutes: false,\n frameworkErrors: (error, request, reply) => {\n this.#reply(reply, this.#dispatchFallback(request, reply, error)).catch(() => undefined)\n },\n ...serverOptions,\n routerOptions: { maxParamLength: MAX_PARAM_LENGTH, ...serverOptions.routerOptions },\n })\n } else {\n this.instance = options.instance\n }\n this.#tracker = new ConnectionTracker(this.instance.server)\n }\n\n registerRoute(route: AdapterRoute): void {\n if (this.#ready !== undefined) throw new Error('fastifyAdapter: routes must be registered before listen()/inject()')\n this.#routes.push(route)\n }\n\n setFallbackHandler(handler: FallbackHandler): void {\n this.#fallback = handler\n }\n\n #prepare(): Promise<void> {\n this.#ready ??= this.#register()\n return this.#ready\n }\n\n async #register(): Promise<void> {\n const routes = this.#routes\n await this.instance.register((child, _options, done) => {\n // Znight reads and parses bodies itself: accept every content type without consuming it\n child.removeAllContentTypeParsers()\n child.addContentTypeParser('*', (_request, _payload, done) => done(null, undefined))\n // Fastify answers 415 to a content type it cannot parse (`foo`, an empty one) before the route\n // runs: hide such a header from Fastify (Znight reads it, and parses the body as bytes)\n child.addHook('onRequest', (request, _reply, done) => {\n const contentType = request.headers['content-type']\n if (contentType !== undefined && request.mediaType === undefined) {\n HIDDEN_CONTENT_TYPES.set(request.raw, contentType)\n delete request.raw.headers['content-type']\n }\n done()\n })\n for (const route of routes) {\n const handler = route.handler\n child.route({\n method: route.method,\n url: toFastifyPath(route.path),\n exposeHeadRoute: false,\n handler: (request, reply) => this.#reply(reply, handler(adapterRequest(request, reply))),\n })\n }\n child.setNotFoundHandler((request, reply) =>\n this.#reply(reply, this.#dispatchFallback(request, reply, undefined)),\n )\n child.setErrorHandler((error, request, reply) =>\n this.#reply(reply, this.#dispatchFallback(request, reply, error)),\n )\n done()\n })\n await this.instance.ready()\n }\n\n #dispatchFallback(request: FastifyRequest, reply: FastifyReply, error: unknown): Promise<AdapterReply> {\n if (this.#fallback === undefined) return Promise.reject(error ?? new Error('No fallback handler'))\n return this.#fallback(adapterRequest(request, undefined, reply), error)\n }\n\n async #reply(reply: FastifyReply, pending: ReturnType<AdapterHandler>): Promise<FastifyReply> {\n let result: AdapterReply\n try {\n result = await pending\n } catch {\n result = {\n status: 500,\n headers: { 'content-type': 'application/json; charset=utf-8' },\n body: new TextEncoder().encode('{\"statusCode\":500,\"error\":\"Internal Server Error\"}'),\n }\n }\n if (result.raw === true || reply.sent || reply.raw.destroyed) {\n discardStream(result.body instanceof ReadableStream ? result.body : null)\n return reply\n }\n if (result.eventStream === true) {\n // no onSend hook (compression, ETag...) may rewrite an event stream: write it on the raw\n // response, with the headers Fastify hooks already set (`onRequest`...) under the plugin's,\n // and their cookies before the plugin's (as reply.header('set-cookie') does on the other path)\n const native = lowerCaseHeaders(reply.getHeaders())\n const headers = { ...native, ...withNativeCookies(native['set-cookie'], result.headers) }\n reply.hijack()\n writeNodeReply(reply.raw, { ...result, headers })\n return reply\n }\n reply.code(result.status)\n for (const name of Object.keys(result.headers)) reply.header(name, result.headers[name])\n const body = result.body\n if (body === null) return reply.send()\n if (body instanceof Uint8Array) {\n // a string is written with the headers at once (bytes take a second write)\n if (result.text !== undefined && sendsTextAsIs(result.headers['content-type'])) return reply.send(result.text)\n return reply.send(Buffer.isBuffer(body) ? body : Buffer.from(body.buffer, body.byteOffset, body.byteLength))\n }\n const stream = body instanceof Blob ? body.stream() : body\n return reply.send(Readable.fromWeb(stream as import('node:stream/web').ReadableStream))\n }\n\n async listen(port: number, host: string): Promise<HttpServerAddress> {\n await this.#prepare()\n this.#closing = undefined\n await this.instance.listen({ port, host })\n const address = this.instance.server.address()\n if (address === null || typeof address === 'string') return serverAddress(host, port)\n return serverAddress(address.address, address.port, address.family)\n }\n\n close(options: AdapterCloseOptions = {}): Promise<void> {\n if (this.#closing === undefined) {\n this.#tracker.closeIdle()\n this.#closing = this.instance.close()\n }\n if (options.force === true) {\n this.instance.server.closeAllConnections?.()\n this.#tracker.destroyAll()\n }\n return this.#closing\n }\n\n inject(request: InjectRequest): Promise<InjectResponse> {\n return bufferInjectStream(stream => this.injectStream(stream), request)\n }\n\n async injectStream(request: InjectStreamRequest): Promise<InjectStreamResponse> {\n await this.#prepare()\n const options: LightInjectOptions = await lightInjectOptions(request)\n // Fastify forwards the options to light-my-request (`payloadAsStream`)\n const inject = this.instance.inject as unknown as (options: LightInjectOptions) => Promise<unknown>\n const response = await inject.call(this.instance, options)\n return lightStreamResponse(response as Parameters<typeof lightStreamResponse>[0], request.signal)\n }\n}\n\n/** The JSON content type of the plugin (with a charset). */\nconst JSON_TYPE = 'application/json; charset=utf-8'\n\n/**\n * Whether Fastify sends a string with this content type unchanged: it adds a charset to a JSON\n * type without one, and a content type to a string without one.\n */\nfunction sendsTextAsIs(type: HeaderValue | undefined): boolean {\n if (typeof type !== 'string') return false\n if (type === JSON_TYPE) return true\n const semicolon = type.indexOf(';')\n return !(semicolon === -1 ? type : type.slice(0, semicolon)).toLowerCase().includes('json')\n}\n\nfunction lowerCaseHeaders(\n headers: Readonly<Record<string, string | number | readonly string[] | undefined>>,\n): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {}\n for (const [name, value] of Object.entries(headers)) {\n if (value !== undefined) result[name.toLowerCase()] = typeof value === 'object' ? [...value] : String(value)\n }\n return result\n}\n\nfunction adapterRequest(\n request: FastifyRequest,\n reply: FastifyReply | undefined,\n response: FastifyReply | undefined = reply,\n): AdapterRequest {\n const hidden = HIDDEN_CONTENT_TYPES.get(request.raw)\n if (hidden !== undefined) {\n // Fastify is done with the content type: restore it (for Znight and `@Req()`)\n HIDDEN_CONTENT_TYPES.delete(request.raw)\n request.raw.headers['content-type'] = hidden\n }\n return new FastifyAdapterRequest(request, reply, response)\n}\n\n/** A Fastify request as an {@link AdapterRequest} (a class: no closure per request). */\nclass FastifyAdapterRequest implements AdapterRequest {\n readonly method: string\n readonly url: string\n readonly headers: Readonly<Record<string, string | undefined>>\n readonly ip: string | undefined\n readonly remoteAddress: string | undefined\n readonly encrypted: boolean\n readonly raw: FastifyRequest\n /** `@Res()`: the reply of a route, `undefined` for the fallback handler. */\n readonly rawResponse: FastifyReply | undefined\n /** The reply whose disconnection aborts the request. */\n readonly #response: FastifyReply | undefined\n #body: Promise<Uint8Array> | undefined\n\n constructor(request: FastifyRequest, reply: FastifyReply | undefined, response: FastifyReply | undefined) {\n this.method = request.raw.method ?? request.method\n this.url = request.raw.url ?? request.url\n this.headers = normalizeNodeHeaders(request.headers)\n this.ip = request.ip\n this.remoteAddress = request.raw.socket?.remoteAddress\n this.encrypted = isEncrypted(request.raw)\n this.raw = request\n this.rawResponse = reply\n this.#response = response\n }\n\n readBody(limit: number): Promise<Uint8Array> {\n this.#body ??= readNodeBody(this.raw.raw, limit)\n return this.#body\n }\n\n streamBody(limit: number): ReadableStream<Uint8Array> {\n return nodeBodyStream(this.raw.raw, limit)\n }\n\n rawStatus(): number | undefined {\n return this.rawResponse?.statusCode\n }\n\n onAbort(listener: () => void): void {\n if (this.#response !== undefined) onResponseAbort(this.#response.raw, listener)\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAgFA,SAAgB,eAAe,UAAiC,CAAC,GAAmB;CAClF,OAAO,IAAI,mBAAmB,OAAO;AACvC;AAEA,MAAM,mBAAmB;;AAGzB,MAAM,uCAAuB,IAAI,QAAwB;AAEzD,IAAM,qBAAN,MAAmD;CACjD,OAAgB;CAChB;CACA,UAAmC,CAAC;CACpC;CACA;CACA;CACA;CAEA,YAAY,SAAgC;EAC1C,IAAI,QAAQ,aAAa,KAAA,GAAW;GAClC,MAAM,gBAAgB,QAAQ,WAAW,CAAC;GAC1C,KAAK,WAAW,QAAQ;IACtB,kBAAkB;IAClB,kBAAkB,OAAO,SAAS,UAAU;KAC1C,KAAK,OAAO,OAAO,KAAK,kBAAkB,SAAS,OAAO,KAAK,CAAC,CAAC,CAAC,YAAY,KAAA,CAAS;IACzF;IACA,GAAG;IACH,eAAe;KAAE,gBAAgB;KAAkB,GAAG,cAAc;IAAc;GACpF,CAAC;EACH,OACE,KAAK,WAAW,QAAQ;EAE1B,KAAK,WAAW,IAAI,kBAAkB,KAAK,SAAS,MAAM;CAC5D;CAEA,cAAc,OAA2B;EACvC,IAAI,KAAK,WAAW,KAAA,GAAW,MAAM,IAAI,MAAM,oEAAoE;EACnH,KAAK,QAAQ,KAAK,KAAK;CACzB;CAEA,mBAAmB,SAAgC;EACjD,KAAK,YAAY;CACnB;CAEA,WAA0B;EACxB,KAAK,WAAW,KAAK,UAAU;EAC/B,OAAO,KAAK;CACd;CAEA,MAAM,YAA2B;EAC/B,MAAM,SAAS,KAAK;EACpB,MAAM,KAAK,SAAS,UAAU,OAAO,UAAU,SAAS;GAEtD,MAAM,4BAA4B;GAClC,MAAM,qBAAqB,MAAM,UAAU,UAAU,SAAS,KAAK,MAAM,KAAA,CAAS,CAAC;GAGnF,MAAM,QAAQ,cAAc,SAAS,QAAQ,SAAS;IACpD,MAAM,cAAc,QAAQ,QAAQ;IACpC,IAAI,gBAAgB,KAAA,KAAa,QAAQ,cAAc,KAAA,GAAW;KAChE,qBAAqB,IAAI,QAAQ,KAAK,WAAW;KACjD,OAAO,QAAQ,IAAI,QAAQ;IAC7B;IACA,KAAK;GACP,CAAC;GACD,KAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,UAAU,MAAM;IACtB,MAAM,MAAM;KACV,QAAQ,MAAM;KACd,KAAK,cAAc,MAAM,IAAI;KAC7B,iBAAiB;KACjB,UAAU,SAAS,UAAU,KAAK,OAAO,OAAO,QAAQ,eAAe,SAAS,KAAK,CAAC,CAAC;IACzF,CAAC;GACH;GACA,MAAM,oBAAoB,SAAS,UACjC,KAAK,OAAO,OAAO,KAAK,kBAAkB,SAAS,OAAO,KAAA,CAAS,CAAC,CACtE;GACA,MAAM,iBAAiB,OAAO,SAAS,UACrC,KAAK,OAAO,OAAO,KAAK,kBAAkB,SAAS,OAAO,KAAK,CAAC,CAClE;GACA,KAAK;EACP,CAAC;EACD,MAAM,KAAK,SAAS,MAAM;CAC5B;CAEA,kBAAkB,SAAyB,OAAqB,OAAuC;EACrG,IAAI,KAAK,cAAc,KAAA,GAAW,OAAO,QAAQ,OAAO,yBAAS,IAAI,MAAM,qBAAqB,CAAC;EACjG,OAAO,KAAK,UAAU,eAAe,SAAS,KAAA,GAAW,KAAK,GAAG,KAAK;CACxE;CAEA,MAAM,OAAO,OAAqB,SAA4D;EAC5F,IAAI;EACJ,IAAI;GACF,SAAS,MAAM;EACjB,QAAQ;GACN,SAAS;IACP,QAAQ;IACR,SAAS,EAAE,gBAAgB,kCAAkC;IAC7D,MAAM,IAAI,YAAY,CAAC,CAAC,OAAO,0DAAoD;GACrF;EACF;EACA,IAAI,OAAO,QAAQ,QAAQ,MAAM,QAAQ,MAAM,IAAI,WAAW;GAC5D,cAAc,OAAO,gBAAgB,iBAAiB,OAAO,OAAO,IAAI;GACxE,OAAO;EACT;EACA,IAAI,OAAO,gBAAgB,MAAM;GAI/B,MAAM,SAAS,iBAAiB,MAAM,WAAW,CAAC;GAClD,MAAM,UAAU;IAAE,GAAG;IAAQ,GAAG,kBAAkB,OAAO,eAAe,OAAO,OAAO;GAAE;GACxF,MAAM,OAAO;GACb,eAAe,MAAM,KAAK;IAAE,GAAG;IAAQ;GAAQ,CAAC;GAChD,OAAO;EACT;EACA,MAAM,KAAK,OAAO,MAAM;EACxB,KAAK,MAAM,QAAQ,OAAO,KAAK,OAAO,OAAO,GAAG,MAAM,OAAO,MAAM,OAAO,QAAQ,KAAK;EACvF,MAAM,OAAO,OAAO;EACpB,IAAI,SAAS,MAAM,OAAO,MAAM,KAAK;EACrC,IAAI,gBAAgB,YAAY;GAE9B,IAAI,OAAO,SAAS,KAAA,KAAa,cAAc,OAAO,QAAQ,eAAe,GAAG,OAAO,MAAM,KAAK,OAAO,IAAI;GAC7G,OAAO,MAAM,KAAK,OAAO,SAAS,IAAI,IAAI,OAAO,OAAO,KAAK,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU,CAAC;EAC7G;EACA,MAAM,SAAS,gBAAgB,OAAO,KAAK,OAAO,IAAI;EACtD,OAAO,MAAM,KAAK,SAAS,QAAQ,MAAkD,CAAC;CACxF;CAEA,MAAM,OAAO,MAAc,MAA0C;EACnE,MAAM,KAAK,SAAS;EACpB,KAAK,WAAW,KAAA;EAChB,MAAM,KAAK,SAAS,OAAO;GAAE;GAAM;EAAK,CAAC;EACzC,MAAM,UAAU,KAAK,SAAS,OAAO,QAAQ;EAC7C,IAAI,YAAY,QAAQ,OAAO,YAAY,UAAU,OAAO,cAAc,MAAM,IAAI;EACpF,OAAO,cAAc,QAAQ,SAAS,QAAQ,MAAM,QAAQ,MAAM;CACpE;CAEA,MAAM,UAA+B,CAAC,GAAkB;EACtD,IAAI,KAAK,aAAa,KAAA,GAAW;GAC/B,KAAK,SAAS,UAAU;GACxB,KAAK,WAAW,KAAK,SAAS,MAAM;EACtC;EACA,IAAI,QAAQ,UAAU,MAAM;GAC1B,KAAK,SAAS,OAAO,sBAAsB;GAC3C,KAAK,SAAS,WAAW;EAC3B;EACA,OAAO,KAAK;CACd;CAEA,OAAO,SAAiD;EACtD,OAAO,oBAAmB,WAAU,KAAK,aAAa,MAAM,GAAG,OAAO;CACxE;CAEA,MAAM,aAAa,SAA6D;EAC9E,MAAM,KAAK,SAAS;EACpB,MAAM,UAA8B,MAAM,mBAAmB,OAAO;EAGpE,MAAM,WAAW,MADF,KAAK,SAAS,OACC,KAAK,KAAK,UAAU,OAAO;EACzD,OAAO,oBAAoB,UAAuD,QAAQ,MAAM;CAClG;AACF;;AAGA,MAAM,YAAY;;;;;AAMlB,SAAS,cAAc,MAAwC;CAC7D,IAAI,OAAO,SAAS,UAAU,OAAO;CACrC,IAAI,SAAS,WAAW,OAAO;CAC/B,MAAM,YAAY,KAAK,QAAQ,GAAG;CAClC,OAAO,EAAE,cAAc,KAAK,OAAO,KAAK,MAAM,GAAG,SAAS,EAAA,CAAG,YAAY,CAAC,CAAC,SAAS,MAAM;AAC5F;AAEA,SAAS,iBACP,SACmC;CACnC,MAAM,SAA4C,CAAC;CACnD,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAChD,IAAI,UAAU,KAAA,GAAW,OAAO,KAAK,YAAY,KAAK,OAAO,UAAU,WAAW,CAAC,GAAG,KAAK,IAAI,OAAO,KAAK;CAE7G,OAAO;AACT;AAEA,SAAS,eACP,SACA,OACA,WAAqC,OACrB;CAChB,MAAM,SAAS,qBAAqB,IAAI,QAAQ,GAAG;CACnD,IAAI,WAAW,KAAA,GAAW;EAExB,qBAAqB,OAAO,QAAQ,GAAG;EACvC,QAAQ,IAAI,QAAQ,kBAAkB;CACxC;CACA,OAAO,IAAI,sBAAsB,SAAS,OAAO,QAAQ;AAC3D;;AAGA,IAAM,wBAAN,MAAsD;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;;CAEA;;CAEA;CACA;CAEA,YAAY,SAAyB,OAAiC,UAAoC;EACxG,KAAK,SAAS,QAAQ,IAAI,UAAU,QAAQ;EAC5C,KAAK,MAAM,QAAQ,IAAI,OAAO,QAAQ;EACtC,KAAK,UAAU,qBAAqB,QAAQ,OAAO;EACnD,KAAK,KAAK,QAAQ;EAClB,KAAK,gBAAgB,QAAQ,IAAI,QAAQ;EACzC,KAAK,YAAY,YAAY,QAAQ,GAAG;EACxC,KAAK,MAAM;EACX,KAAK,cAAc;EACnB,KAAK,YAAY;CACnB;CAEA,SAAS,OAAoC;EAC3C,KAAK,UAAU,aAAa,KAAK,IAAI,KAAK,KAAK;EAC/C,OAAO,KAAK;CACd;CAEA,WAAW,OAA2C;EACpD,OAAO,eAAe,KAAK,IAAI,KAAK,KAAK;CAC3C;CAEA,YAAgC;EAC9B,OAAO,KAAK,aAAa;CAC3B;CAEA,QAAQ,UAA4B;EAClC,IAAI,KAAK,cAAc,KAAA,GAAW,gBAAgB,KAAK,UAAU,KAAK,QAAQ;CAChF;AACF"}
|
package/dist/hono.d.mts
CHANGED
package/dist/hono.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { N as discardStream, b as webBodyStream, f as bufferInjectStream, n as closeServer, p as injectFetchStream, s as onResponseAbort, t as ConnectionTracker, v as readWebStream, y as serverAddress } from "./node-io-
|
|
1
|
+
import { N as discardStream, a as nodeBodyStream, b as webBodyStream, c as readNodeBody, f as bufferInjectStream, n as closeServer, p as injectFetchStream, s as onResponseAbort, t as ConnectionTracker, v as readWebStream, y as serverAddress } from "./node-io-CVnqpVNS.mjs";
|
|
2
2
|
import { r as isBun } from "./utils-DCbu3a4w.mjs";
|
|
3
3
|
import { toHonoPath } from "./path.mjs";
|
|
4
4
|
import { Hono } from "hono";
|
|
@@ -25,6 +25,8 @@ var HonoHttpAdapter = class {
|
|
|
25
25
|
name = "hono";
|
|
26
26
|
app;
|
|
27
27
|
#serveOptions;
|
|
28
|
+
/** Number of routes of `app` registered by the adapter (see {@link #middleware}). */
|
|
29
|
+
#routes = 0;
|
|
28
30
|
#fallback;
|
|
29
31
|
#prepared = false;
|
|
30
32
|
#bun;
|
|
@@ -42,6 +44,7 @@ var HonoHttpAdapter = class {
|
|
|
42
44
|
registerRoute(route) {
|
|
43
45
|
const handler = route.handler;
|
|
44
46
|
const path = toHonoPath(route.path);
|
|
47
|
+
this.#routes++;
|
|
45
48
|
if (route.method === "HEAD") {
|
|
46
49
|
this.app.get(path, (c, next) => c.req.raw.method === "HEAD" ? this.#serve(c, handler) : next().then(() => c.res));
|
|
47
50
|
return;
|
|
@@ -66,7 +69,15 @@ var HonoHttpAdapter = class {
|
|
|
66
69
|
if (this.#fallback === void 0) throw error ?? /* @__PURE__ */ new Error("No fallback handler");
|
|
67
70
|
return this.#toResponse(c, await this.#fallback(adapterRequest(c), error));
|
|
68
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether `app` has routes the adapter did not register (middleware, other routes): only they can
|
|
74
|
+
* set headers on the context or read the response.
|
|
75
|
+
*/
|
|
76
|
+
#middleware() {
|
|
77
|
+
return this.app.routes.length !== this.#routes;
|
|
78
|
+
}
|
|
69
79
|
#toResponse(c, reply) {
|
|
80
|
+
const middleware = this.#middleware();
|
|
70
81
|
const outgoing = nodeOutgoing(c);
|
|
71
82
|
if (outgoing !== void 0) {
|
|
72
83
|
const body = reply.body instanceof ReadableStream ? reply.body : null;
|
|
@@ -75,14 +86,14 @@ var HonoHttpAdapter = class {
|
|
|
75
86
|
return toResponse(c, {
|
|
76
87
|
...reply,
|
|
77
88
|
body: null
|
|
78
|
-
});
|
|
89
|
+
}, middleware);
|
|
79
90
|
}
|
|
80
91
|
if (body !== null) outgoing.once("close", () => {
|
|
81
92
|
if (!outgoing.writableFinished) discardStream(body);
|
|
82
93
|
});
|
|
83
94
|
if (reply.eventStream === true && typeof outgoing.setTimeout === "function") outgoing.setTimeout(0);
|
|
84
95
|
}
|
|
85
|
-
if (this.#bun === void 0 || !(reply.body instanceof ReadableStream)) return toResponse(c, reply);
|
|
96
|
+
if (this.#bun === void 0 || !(reply.body instanceof ReadableStream)) return toResponse(c, reply, middleware);
|
|
86
97
|
const reader = reply.body.getReader();
|
|
87
98
|
let ended = false;
|
|
88
99
|
let onEnd = () => void 0;
|
|
@@ -121,7 +132,7 @@ var HonoHttpAdapter = class {
|
|
|
121
132
|
return toResponse(c, {
|
|
122
133
|
...reply,
|
|
123
134
|
body
|
|
124
|
-
});
|
|
135
|
+
}, middleware);
|
|
125
136
|
}
|
|
126
137
|
/** The fetch handler given to `Bun.serve()`. */
|
|
127
138
|
async #bunFetch(request, server, idleTimeout) {
|
|
@@ -209,38 +220,87 @@ var HonoHttpAdapter = class {
|
|
|
209
220
|
}
|
|
210
221
|
};
|
|
211
222
|
function adapterRequest(c) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
223
|
+
return new HonoAdapterRequest(c);
|
|
224
|
+
}
|
|
225
|
+
/** A Hono request as an {@link AdapterRequest} (a class: no closure per request). */
|
|
226
|
+
var HonoAdapterRequest = class {
|
|
227
|
+
method;
|
|
228
|
+
url;
|
|
229
|
+
headers;
|
|
230
|
+
ip;
|
|
231
|
+
remoteAddress;
|
|
232
|
+
encrypted;
|
|
233
|
+
raw;
|
|
234
|
+
rawResponse;
|
|
235
|
+
/** The Node.js request of `@hono/node-server`, absent on Bun and for `inject()`. */
|
|
236
|
+
#incoming;
|
|
237
|
+
/**
|
|
238
|
+
* The same request over HTTP/1.1, read directly (Node.js streams): no Fetch `Request` and body
|
|
239
|
+
* stream are built for it.
|
|
240
|
+
*/
|
|
241
|
+
#http1;
|
|
242
|
+
#body;
|
|
243
|
+
constructor(c) {
|
|
244
|
+
const request = c.req.raw;
|
|
245
|
+
const incoming = nodeIncoming(c);
|
|
246
|
+
const http1 = incoming !== void 0 && incoming.httpVersionMajor < 2 ? incoming : void 0;
|
|
247
|
+
const address = remoteAddress(c);
|
|
248
|
+
this.method = request.method;
|
|
249
|
+
this.url = pathAndQuery(typeof incoming?.url === "string" && incoming.url !== "" ? incoming.url : request.url);
|
|
250
|
+
this.headers = (http1 === void 0 ? void 0 : distinctHeaders(http1)) ?? fetchHeaders(request.headers);
|
|
251
|
+
this.ip = address;
|
|
252
|
+
this.remoteAddress = address;
|
|
253
|
+
this.encrypted = incoming === void 0 ? request.url.startsWith("https:") : incoming.socket?.encrypted === true;
|
|
254
|
+
this.raw = request;
|
|
255
|
+
this.rawResponse = c;
|
|
256
|
+
this.#incoming = incoming;
|
|
257
|
+
this.#http1 = http1;
|
|
258
|
+
}
|
|
259
|
+
readBody(limit) {
|
|
260
|
+
if (this.#body === void 0) {
|
|
261
|
+
const http1 = this.#http1;
|
|
262
|
+
this.#body = http1 === void 0 ? readWebStream(() => this.raw.body, limit, this.headers["content-length"]) : readNodeBody(http1, limit);
|
|
242
263
|
}
|
|
243
|
-
|
|
264
|
+
return this.#body;
|
|
265
|
+
}
|
|
266
|
+
streamBody(limit) {
|
|
267
|
+
const http1 = this.#http1;
|
|
268
|
+
if (http1 !== void 0) return nodeBodyStream(http1, limit);
|
|
269
|
+
return webBodyStream(() => this.raw.body, limit, this.headers["content-length"]);
|
|
270
|
+
}
|
|
271
|
+
hasBody() {
|
|
272
|
+
const incoming = this.#incoming;
|
|
273
|
+
return incoming === void 0 ? this.raw.body !== null : incoming.httpVersionMajor >= 2 && incoming.stream?.endAfterHeaders === false;
|
|
274
|
+
}
|
|
275
|
+
onAbort(listener) {
|
|
276
|
+
const outgoing = nodeOutgoing(this.rawResponse);
|
|
277
|
+
if (outgoing !== void 0) {
|
|
278
|
+
onResponseAbort(outgoing, listener);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const signal = this.raw.signal;
|
|
282
|
+
if (signal.aborted) queueMicrotask(listener);
|
|
283
|
+
else signal.addEventListener("abort", listener, { once: true });
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
/** The headers of a Fetch request: lower-case names, repeated headers joined (`Headers` semantics). */
|
|
287
|
+
function fetchHeaders(source) {
|
|
288
|
+
const headers = {};
|
|
289
|
+
for (const [name, value] of source) headers[name] = value;
|
|
290
|
+
return headers;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The headers of a Node.js request when none is repeated: then they hold the values of its Fetch
|
|
294
|
+
* `Headers` (Node.js keeps the first of some repeated headers where `Headers` join them all).
|
|
295
|
+
*/
|
|
296
|
+
function distinctHeaders(request) {
|
|
297
|
+
const headers = request.headers;
|
|
298
|
+
let count = 0;
|
|
299
|
+
for (const name in headers) {
|
|
300
|
+
if (typeof headers[name] !== "string") return void 0;
|
|
301
|
+
count++;
|
|
302
|
+
}
|
|
303
|
+
return count * 2 === request.rawHeaders.length ? headers : void 0;
|
|
244
304
|
}
|
|
245
305
|
/** The Node.js response of `@hono/node-server` (`c.env.outgoing`), absent on Bun and for `inject()`. */
|
|
246
306
|
function nodeOutgoing(c) {
|
|
@@ -268,11 +328,20 @@ function remoteAddress(c) {
|
|
|
268
328
|
if (typeof env.requestIP === "function") return env.requestIP(c.req.raw)?.address ?? void 0;
|
|
269
329
|
return env.incoming?.socket?.remoteAddress;
|
|
270
330
|
}
|
|
271
|
-
|
|
331
|
+
/**
|
|
332
|
+
* The `Response` of a reply. Without `middleware` (see `#middleware`), nothing set headers on the
|
|
333
|
+
* context and nothing reads the response: headers given as a record of strings are sent as is by
|
|
334
|
+
* `@hono/node-server` (no `Headers` object), and a text body as a string.
|
|
335
|
+
*/
|
|
336
|
+
function toResponse(c, reply, middleware) {
|
|
272
337
|
if (reply.raw === true) {
|
|
273
338
|
discardStream(reply.body instanceof ReadableStream ? reply.body : null);
|
|
274
339
|
return c.res;
|
|
275
340
|
}
|
|
341
|
+
if (!middleware && isStringRecord(reply.headers)) return new Response(reply.text ?? reply.body, {
|
|
342
|
+
status: reply.status,
|
|
343
|
+
headers: reply.headers
|
|
344
|
+
});
|
|
276
345
|
const headers = new Headers();
|
|
277
346
|
try {
|
|
278
347
|
for (const name of Object.keys(reply.headers)) {
|
|
@@ -287,12 +356,17 @@ function toResponse(c, reply) {
|
|
|
287
356
|
headers: { "content-type": "application/json; charset=utf-8" }
|
|
288
357
|
});
|
|
289
358
|
}
|
|
290
|
-
keepContextCookies(c, headers);
|
|
291
|
-
return new Response(reply.body, {
|
|
359
|
+
if (middleware) keepContextCookies(c, headers);
|
|
360
|
+
return new Response(reply.text ?? reply.body, {
|
|
292
361
|
status: reply.status,
|
|
293
362
|
headers
|
|
294
363
|
});
|
|
295
364
|
}
|
|
365
|
+
/** Whether every header has a single value (a record of strings is a valid `HeadersInit`). */
|
|
366
|
+
function isStringRecord(headers) {
|
|
367
|
+
for (const name in headers) if (typeof headers[name] !== "string") return false;
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
296
370
|
/**
|
|
297
371
|
* Keeps the cookies Hono middleware set before the Znight route (`setCookie(c, ...)`,
|
|
298
372
|
* `c.header('set-cookie', ..., { append: true })`) before the reply's own, like the other adapters.
|
package/dist/hono.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hono.mjs","names":[],"sources":["../src/adapters/hono.ts"],"sourcesContent":["/**\n * `@znight/http/hono`: serves Znight routes with [Hono](https://hono.dev) 4 (optional peer\n * dependency `hono`). On Bun the server is `Bun.serve()`; on Node.js it is `@hono/node-server`\n * (optional peer dependency, loaded on `listen()`). `inject()` uses `app.request()`.\n */\nimport type { Server, ServerResponse } from 'node:http'\nimport type {\n AdapterCloseOptions,\n AdapterHandler,\n AdapterReply,\n AdapterRequest,\n AdapterRoute,\n FallbackHandler,\n HttpAdapter,\n HttpServerAddress,\n InjectRequest,\n InjectResponse,\n InjectStreamRequest,\n InjectStreamResponse,\n} from '../adapter.ts'\n\nimport { type Context, Hono, type Next } from 'hono'\n\nimport { toHonoPath } from '../path.ts'\nimport { discardStream } from '../stream.ts'\nimport { isBun } from '../utils.ts'\nimport { ConnectionTracker, closeServer, onResponseAbort } from './node-io.ts'\nimport { bufferInjectStream, injectFetchStream, readWebStream, serverAddress, webBodyStream } from './shared.ts'\n\n/** Options of {@link honoAdapter}. */\nexport interface HonoAdapterOptions {\n /**\n * An existing application: its middleware (`app.use(...)`) runs before the Znight routes. Znight\n * sets its `notFound` and `onError` handlers.\n */\n readonly app?: Hono\n /**\n * Options of `Bun.serve()` on Bun (`tls`, `idleTimeout`, `development`, `error`...); Znight sets\n * `port`, `hostname` and `fetch`. Ignored on Node.js.\n */\n readonly serveOptions?: HonoServeOptions\n}\n\n/**\n * Options of `Bun.serve()` (see the Bun documentation; Bun types are not required). Defaults set by\n * the adapter: `maxRequestBodySize` unlimited (Znight enforces `bodyLimit` / `@BodyLimit` itself)\n * and Bun's `idleTimeout` (10 s), lifted while a streamed response is sent.\n */\nexport interface HonoServeOptions {\n /** Seconds a connection may stay idle (`0`: never closed). Default: 10. */\n readonly idleTimeout?: number\n /** Request bodies above this size are refused by Bun (413). Default: unlimited. */\n readonly maxRequestBodySize?: number\n /** TLS options (`{ key, cert }`). */\n readonly tls?: unknown\n /** Any other `Bun.serve()` option. */\n readonly [option: string]: unknown\n}\n\n/** The Hono adapter. */\nexport interface HonoAdapter extends HttpAdapter {\n readonly name: 'hono'\n /** The Hono application. */\n readonly app: Hono\n /** Fetch handler of the application (`export default { fetch: adapter.fetch }` on edge runtimes). */\n readonly fetch: (request: Request, env?: unknown) => Response | Promise<Response>\n}\n\n/**\n * Creates a Hono adapter.\n *\n * ```ts\n * import { honoAdapter } from '@znight/http/hono'\n *\n * HttpPlugin.configure({ adapter: honoAdapter() })\n * ```\n */\nexport function honoAdapter(options: HonoAdapterOptions = {}): HonoAdapter {\n return new HonoHttpAdapter(options)\n}\n\n/** The parts of a Bun server used here (Bun types are not available to the package sources). */\ninterface BunServer {\n readonly port: number\n readonly hostname: string\n stop(closeActiveConnections?: boolean): Promise<void> | void\n requestIP?(request: Request): { readonly address: string } | null\n /** Idle timeout of one request, in seconds (`0`: none). */\n timeout?(request: Request, seconds: number): void\n}\n\ninterface BunRuntime {\n serve(\n options: HonoServeOptions & {\n port: number\n hostname: string\n fetch: (request: Request, server: BunServer) => Response | Promise<Response>\n },\n ): BunServer\n}\n\n/** Default `idleTimeout` of `Bun.serve()`, in seconds. */\nconst BUN_IDLE_TIMEOUT = 10\n\n/** A response body built while serving with `Bun.serve()` (see `#bunFetch`). */\ninterface BunBody {\n /** The `Blob` (file) the body reads. */\n readonly blob: Blob | undefined\n /** Sets the function called when the body ends (read to its end, failed or cancelled). */\n onEnd(callback: () => void): void\n}\n\n/** Response bodies built while serving with `Bun.serve()`, by body (Hono may copy a `Response`, not its body). */\nconst BUN_BODIES = new WeakMap<ReadableStream<Uint8Array>, BunBody>()\n\n/** The Node.js request behind a request of `@hono/node-server`. */\ninterface NodeIncoming {\n readonly url?: string\n readonly socket?: { readonly encrypted?: unknown }\n readonly httpVersionMajor: number\n readonly stream?: { readonly endAfterHeaders?: boolean }\n}\n\n/** Bindings of injected requests (the remote address the other adapters report for `inject()`). */\nconst INJECT_ENV = Object.freeze({ incoming: Object.freeze({ socket: Object.freeze({ remoteAddress: '127.0.0.1' }) }) })\n\ntype HonoMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'\n\nclass HonoHttpAdapter implements HonoAdapter {\n readonly name = 'hono' as const\n readonly app: Hono\n readonly #serveOptions: HonoServeOptions\n #fallback: FallbackHandler | undefined\n #prepared = false\n #bun: BunServer | undefined\n #node: Server | undefined\n #tracker: ConnectionTracker | undefined\n #closing: Promise<void> | undefined\n\n constructor(options: HonoAdapterOptions) {\n this.app = options.app ?? new Hono()\n this.#serveOptions = options.serveOptions ?? {}\n }\n\n readonly fetch = (request: Request, env?: unknown): Response | Promise<Response> => {\n this.#prepare()\n return this.app.fetch(request, env)\n }\n\n registerRoute(route: AdapterRoute): void {\n const handler = route.handler\n const path = toHonoPath(route.path)\n if (route.method === 'HEAD') {\n // Hono routes HEAD requests to GET handlers: serve them from a GET handler that only answers HEAD\n this.app.get(path, (c: Context, next: Next) =>\n c.req.raw.method === 'HEAD' ? this.#serve(c, handler) : next().then(() => c.res),\n )\n return\n }\n this.app.on(route.method as HonoMethod, path, (c: Context) => this.#serve(c, handler))\n }\n\n setFallbackHandler(handler: FallbackHandler): void {\n this.#fallback = handler\n }\n\n #prepare(): void {\n if (this.#prepared) return\n this.#prepared = true\n this.app.notFound(c => this.#serveFallback(c, undefined))\n this.app.onError((error, c) => this.#serveFallback(c, error))\n }\n\n async #serve(c: Context, handler: AdapterHandler): Promise<Response> {\n const request = adapterRequest(c)\n // Hono routes the WHATWG-normalised URL (`/a/../b` → `/b`, `%2e%2e` resolved): when the raw\n // target differs (Node.js), let the plugin route the raw path, like on the other adapters\n const reply =\n this.#fallback !== undefined && pathOf(request.url) !== pathOf(pathAndQuery(c.req.raw.url))\n ? await this.#fallback(request)\n : await handler(request)\n return this.#toResponse(c, reply)\n }\n\n async #serveFallback(c: Context, error: unknown): Promise<Response> {\n if (this.#fallback === undefined) throw error ?? new Error('No fallback handler')\n return this.#toResponse(c, await this.#fallback(adapterRequest(c), error))\n }\n\n #toResponse(c: Context, reply: AdapterReply): Response {\n const outgoing = nodeOutgoing(c)\n if (outgoing !== undefined) {\n const body = reply.body instanceof ReadableStream ? reply.body : null\n if (outgoing.destroyed) {\n // @hono/node-server: the client left before the reply was ready (an async pre-flight). The\n // server would neither read nor cancel the body: release it (event source, request scope)\n discardStream(body)\n return toResponse(c, { ...reply, body: null })\n }\n if (body !== null) {\n // the client may leave before @hono/node-server reads the body (its own listener then comes\n // too late): a body nobody locked is cancelled when the response closes unfinished\n outgoing.once('close', () => {\n if (!outgoing.writableFinished) discardStream(body)\n })\n }\n // a quiet event stream must outlive a socket timeout\n if (reply.eventStream === true && typeof outgoing.setTimeout === 'function') outgoing.setTimeout(0)\n }\n if (this.#bun === undefined || !(reply.body instanceof ReadableStream)) return toResponse(c, reply)\n // served by Bun.serve(): observe the end of the body (idle timeout, native blob: see #bunFetch)\n const reader = reply.body.getReader()\n let ended = false\n let onEnd = (): void => undefined\n const end = (): void => {\n if (ended) return\n ended = true\n onEnd()\n }\n const body = new ReadableStream<Uint8Array>(\n {\n async pull(controller) {\n let result: Awaited<ReturnType<typeof reader.read>>\n try {\n result = await reader.read()\n } catch (error) {\n end()\n throw error\n }\n if (result.done) {\n end() // before closing: Bun ignores timeout() once the response is complete\n controller.close()\n return\n }\n controller.enqueue(result.value)\n },\n cancel(reason) {\n end()\n return reader.cancel(reason)\n },\n },\n { highWaterMark: 0 },\n )\n BUN_BODIES.set(body, {\n blob: reply.blob,\n onEnd: callback => {\n onEnd = callback\n },\n })\n return toResponse(c, { ...reply, body })\n }\n\n /** The fetch handler given to `Bun.serve()`. */\n async #bunFetch(request: Request, server: BunServer, idleTimeout: number): Promise<Response> {\n const response = await this.app.fetch(request, server)\n const body = response.body\n const served = body === null ? undefined : BUN_BODIES.get(body)\n if (body === null || served === undefined) return response\n BUN_BODIES.delete(body)\n if (served.blob !== undefined && !body.locked) {\n // a Blob is sent by Bun itself (sendfile, exact Content-Length; a stream would be chunked). Bun\n // opens its file as soon as it takes the response: release the stream (and with it the request\n // scope, which may delete the file) right after\n const native = new Response(served.blob, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n })\n setImmediate(() => discardStream(body))\n return native\n }\n if (idleTimeout > 0 && typeof server.timeout === 'function') {\n // the idle timeout of Bun.serve() also cuts a response whose stream pauses (SSE, slow producers):\n // lift it while the stream is sent, then restore it for the keep-alive connection\n server.timeout(request, 0)\n served.onEnd(() => {\n try {\n server.timeout?.(request, idleTimeout)\n } catch {\n // the connection is gone\n }\n })\n }\n return response\n }\n\n async listen(port: number, host: string): Promise<HttpServerAddress> {\n this.#prepare()\n this.#closing = undefined\n const bun = (globalThis as { Bun?: BunRuntime }).Bun\n if (isBun() && bun !== undefined) {\n const options = this.#serveOptions\n const idleTimeout = typeof options.idleTimeout === 'number' ? options.idleTimeout : BUN_IDLE_TIMEOUT\n const server = bun.serve({\n // Znight enforces the body limits (route @BodyLimit, plugin bodyLimit): not Bun's 128 MiB default\n maxRequestBodySize: Number.MAX_SAFE_INTEGER,\n ...options,\n port,\n hostname: host,\n fetch: (request, server) => this.#bunFetch(request, server, idleTimeout),\n })\n this.#bun = server\n return serverAddress(server.hostname, server.port)\n }\n let nodeServer: typeof import('@hono/node-server')\n try {\n nodeServer = await import('@hono/node-server')\n } catch (error) {\n throw new Error(\n 'honoAdapter on Node.js requires the optional peer dependency \"@hono/node-server\" (npm i @hono/node-server)',\n { cause: error },\n )\n }\n const server = nodeServer.createAdaptorServer({ fetch: this.app.fetch }) as Server\n this.#node = server\n this.#tracker = new ConnectionTracker(server)\n await new Promise<void>((resolve, reject) => {\n const onError = (error: Error): void => reject(error)\n server.once('error', onError)\n server.listen(port, host, () => {\n server.off('error', onError)\n resolve()\n })\n })\n const address = server.address()\n if (address === null || typeof address === 'string') return serverAddress(host, port)\n return serverAddress(address.address, address.port, address.family)\n }\n\n close(options: AdapterCloseOptions = {}): Promise<void> {\n const bun = this.#bun\n if (bun !== undefined) {\n if (options.force === true) {\n this.#closing = Promise.resolve(bun.stop(true))\n } else {\n this.#closing ??= Promise.resolve(bun.stop(false))\n }\n return this.#closing\n }\n const server = this.#node\n const tracker = this.#tracker\n if (server === undefined || tracker === undefined) return Promise.resolve()\n this.#closing = closeServer(server, tracker, this.#closing, options.force === true)\n return this.#closing\n }\n\n inject(request: InjectRequest): Promise<InjectResponse> {\n return bufferInjectStream(stream => this.injectStream(stream), request)\n }\n\n injectStream(request: InjectStreamRequest): Promise<InjectStreamResponse> {\n this.#prepare()\n return injectFetchStream(request => this.app.request(request, undefined, INJECT_ENV), request)\n }\n}\n\nfunction adapterRequest(c: Context): AdapterRequest {\n const request = c.req.raw\n const headers: Record<string, string> = {}\n for (const [name, value] of request.headers) headers[name] = value\n const incoming = nodeIncoming(c)\n const address = remoteAddress(c)\n let body: Promise<Uint8Array> | undefined\n return {\n method: request.method,\n // the raw target on Node.js; on Bun only the normalised URL is known\n url: pathAndQuery(typeof incoming?.url === 'string' && incoming.url !== '' ? incoming.url : request.url),\n headers,\n ip: address,\n remoteAddress: address,\n encrypted: incoming === undefined ? request.url.startsWith('https:') : incoming.socket?.encrypted === true,\n raw: request,\n rawResponse: c,\n readBody: (limit: number): Promise<Uint8Array> => {\n body ??= readWebStream(() => request.body, limit, headers['content-length'])\n return body\n },\n streamBody: (limit: number): ReadableStream<Uint8Array> =>\n webBodyStream(() => request.body, limit, headers['content-length']),\n // asked only without content-length / transfer-encoding: a request read from an HTTP/1.1\n // connection then has no body (`@hono/node-server` still gives every POST a body stream), an\n // HTTP/2 one unless its HEADERS frame ended the stream; a Web `Request` (Bun, inject) says it\n hasBody: (): boolean =>\n incoming === undefined\n ? request.body !== null\n : incoming.httpVersionMajor >= 2 && incoming.stream?.endAfterHeaders === false,\n onAbort: (listener: () => void): void => {\n const outgoing = nodeOutgoing(c)\n if (outgoing !== undefined) {\n onResponseAbort(outgoing as unknown as ServerResponse, listener)\n return\n }\n // Bun.serve() and inject(): the request signal aborts when the client leaves\n const signal = request.signal\n if (signal.aborted) queueMicrotask(listener)\n else signal.addEventListener('abort', listener, { once: true })\n },\n }\n}\n\n/** The part of a Node.js `ServerResponse` used here. */\ninterface NodeOutgoing {\n readonly writableFinished: boolean\n readonly destroyed: boolean\n once(event: 'close', listener: () => void): unknown\n setTimeout?(ms: number): unknown\n}\n\n/** The Node.js response of `@hono/node-server` (`c.env.outgoing`), absent on Bun and for `inject()`. */\nfunction nodeOutgoing(c: Context): NodeOutgoing | undefined {\n const outgoing = (c.env as { outgoing?: Partial<NodeOutgoing> } | undefined)?.outgoing\n return typeof outgoing?.once === 'function' ? (outgoing as NodeOutgoing) : undefined\n}\n\n/** The Node.js request of `@hono/node-server` (`c.env.incoming`), absent on Bun and for `inject()`. */\nfunction nodeIncoming(c: Context): NodeIncoming | undefined {\n const incoming = (c.env as { incoming?: Partial<NodeIncoming> } | undefined)?.incoming\n return typeof incoming?.httpVersionMajor === 'number' ? (incoming as NodeIncoming) : undefined\n}\n\nfunction pathAndQuery(url: string): string {\n if (url.startsWith('/')) return url\n const scheme = url.indexOf('://')\n const start = url.indexOf('/', scheme === -1 ? 0 : scheme + 3)\n return start === -1 ? '/' : url.slice(start)\n}\n\nfunction pathOf(url: string): string {\n const query = url.indexOf('?')\n return query === -1 ? url : url.slice(0, query)\n}\n\nfunction remoteAddress(c: Context): string | undefined {\n const env = c.env as\n | { incoming?: { socket?: { remoteAddress?: string } }; requestIP?: BunServer['requestIP'] }\n | undefined\n if (env === undefined || env === null) return undefined\n if (typeof env.requestIP === 'function') return env.requestIP(c.req.raw)?.address ?? undefined\n return env.incoming?.socket?.remoteAddress\n}\n\nfunction toResponse(c: Context, reply: AdapterReply): Response {\n if (reply.raw === true) {\n discardStream(reply.body instanceof ReadableStream ? reply.body : null)\n return c.res\n }\n const headers = new Headers()\n try {\n for (const name of Object.keys(reply.headers)) {\n const value = reply.headers[name]\n if (typeof value === 'string') headers.set(name, value)\n else if (value !== undefined) for (const item of value) headers.append(name, item)\n }\n } catch {\n // a header Headers refuses (the plugin validates them): never rethrow into Hono's onError, which\n // would run the pipeline again; release the body and answer a plain 500, like writeNodeReply\n discardStream(reply.body instanceof ReadableStream ? reply.body : null)\n return new Response('{\"statusCode\":500,\"error\":\"Internal Server Error\"}', {\n status: 500,\n headers: { 'content-type': 'application/json; charset=utf-8' },\n })\n }\n keepContextCookies(c, headers)\n return new Response(reply.body, { status: reply.status, headers })\n}\n\n/**\n * Keeps the cookies Hono middleware set before the Znight route (`setCookie(c, ...)`,\n * `c.header('set-cookie', ..., { append: true })`) before the reply's own, like the other adapters.\n * Hono drops the headers of the context when a handler returns its own `Response` (or, when a\n * middleware read `c.res`, replaces the response's cookies with the context's): the reply's cookies\n * are added to the context too, so that both ways end with every cookie, in order.\n */\nfunction keepContextCookies(c: Context, headers: Headers): void {\n let context: string[]\n try {\n // the headers of the context without changing it (`c.res` would create the context response)\n context = c.newResponse(null).headers.getSetCookie()\n } catch {\n return // a status a bodyless Response refuses (`c.status(101)`): nothing to keep\n }\n if (context.length === 0) return\n const own = headers.getSetCookie()\n for (const line of own) c.header('set-cookie', line, { append: true })\n headers.delete('set-cookie')\n for (const line of context) headers.append('set-cookie', line)\n for (const line of own) headers.append('set-cookie', line)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hono.mjs","names":[],"sources":["../src/adapters/hono.ts"],"sourcesContent":["/**\n * `@znight/http/hono`: serves Znight routes with [Hono](https://hono.dev) 4 (optional peer\n * dependency `hono`). On Bun the server is `Bun.serve()`; on Node.js it is `@hono/node-server`\n * (optional peer dependency, loaded on `listen()`). `inject()` uses `app.request()`.\n */\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http'\nimport type {\n AdapterCloseOptions,\n AdapterHandler,\n AdapterReply,\n AdapterRequest,\n AdapterRoute,\n FallbackHandler,\n HttpAdapter,\n HttpServerAddress,\n InjectRequest,\n InjectResponse,\n InjectStreamRequest,\n InjectStreamResponse,\n} from '../adapter.ts'\nimport type { HeaderValue } from '../exceptions.ts'\n\nimport { type Context, Hono, type Next } from 'hono'\n\nimport { toHonoPath } from '../path.ts'\nimport { discardStream } from '../stream.ts'\nimport { isBun } from '../utils.ts'\nimport { ConnectionTracker, closeServer, nodeBodyStream, onResponseAbort, readNodeBody } from './node-io.ts'\nimport { bufferInjectStream, injectFetchStream, readWebStream, serverAddress, webBodyStream } from './shared.ts'\n\n/** Options of {@link honoAdapter}. */\nexport interface HonoAdapterOptions {\n /**\n * An existing application: its middleware (`app.use(...)`) runs before the Znight routes. Znight\n * sets its `notFound` and `onError` handlers.\n */\n readonly app?: Hono\n /**\n * Options of `Bun.serve()` on Bun (`tls`, `idleTimeout`, `development`, `error`...); Znight sets\n * `port`, `hostname` and `fetch`. Ignored on Node.js.\n */\n readonly serveOptions?: HonoServeOptions\n}\n\n/**\n * Options of `Bun.serve()` (see the Bun documentation; Bun types are not required). Defaults set by\n * the adapter: `maxRequestBodySize` unlimited (Znight enforces `bodyLimit` / `@BodyLimit` itself)\n * and Bun's `idleTimeout` (10 s), lifted while a streamed response is sent.\n */\nexport interface HonoServeOptions {\n /** Seconds a connection may stay idle (`0`: never closed). Default: 10. */\n readonly idleTimeout?: number\n /** Request bodies above this size are refused by Bun (413). Default: unlimited. */\n readonly maxRequestBodySize?: number\n /** TLS options (`{ key, cert }`). */\n readonly tls?: unknown\n /** Any other `Bun.serve()` option. */\n readonly [option: string]: unknown\n}\n\n/** The Hono adapter. */\nexport interface HonoAdapter extends HttpAdapter {\n readonly name: 'hono'\n /** The Hono application. */\n readonly app: Hono\n /** Fetch handler of the application (`export default { fetch: adapter.fetch }` on edge runtimes). */\n readonly fetch: (request: Request, env?: unknown) => Response | Promise<Response>\n}\n\n/**\n * Creates a Hono adapter.\n *\n * ```ts\n * import { honoAdapter } from '@znight/http/hono'\n *\n * HttpPlugin.configure({ adapter: honoAdapter() })\n * ```\n */\nexport function honoAdapter(options: HonoAdapterOptions = {}): HonoAdapter {\n return new HonoHttpAdapter(options)\n}\n\n/** The parts of a Bun server used here (Bun types are not available to the package sources). */\ninterface BunServer {\n readonly port: number\n readonly hostname: string\n stop(closeActiveConnections?: boolean): Promise<void> | void\n requestIP?(request: Request): { readonly address: string } | null\n /** Idle timeout of one request, in seconds (`0`: none). */\n timeout?(request: Request, seconds: number): void\n}\n\ninterface BunRuntime {\n serve(\n options: HonoServeOptions & {\n port: number\n hostname: string\n fetch: (request: Request, server: BunServer) => Response | Promise<Response>\n },\n ): BunServer\n}\n\n/** Default `idleTimeout` of `Bun.serve()`, in seconds. */\nconst BUN_IDLE_TIMEOUT = 10\n\n/** A response body built while serving with `Bun.serve()` (see `#bunFetch`). */\ninterface BunBody {\n /** The `Blob` (file) the body reads. */\n readonly blob: Blob | undefined\n /** Sets the function called when the body ends (read to its end, failed or cancelled). */\n onEnd(callback: () => void): void\n}\n\n/** Response bodies built while serving with `Bun.serve()`, by body (Hono may copy a `Response`, not its body). */\nconst BUN_BODIES = new WeakMap<ReadableStream<Uint8Array>, BunBody>()\n\n/** The Node.js request behind a request of `@hono/node-server`. */\ninterface NodeIncoming {\n readonly url?: string\n readonly socket?: { readonly encrypted?: unknown }\n readonly httpVersionMajor: number\n readonly stream?: { readonly endAfterHeaders?: boolean }\n}\n\n/** Bindings of injected requests (the remote address the other adapters report for `inject()`). */\nconst INJECT_ENV = Object.freeze({ incoming: Object.freeze({ socket: Object.freeze({ remoteAddress: '127.0.0.1' }) }) })\n\ntype HonoMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'\n\nclass HonoHttpAdapter implements HonoAdapter {\n readonly name = 'hono' as const\n readonly app: Hono\n readonly #serveOptions: HonoServeOptions\n /** Number of routes of `app` registered by the adapter (see {@link #middleware}). */\n #routes = 0\n #fallback: FallbackHandler | undefined\n #prepared = false\n #bun: BunServer | undefined\n #node: Server | undefined\n #tracker: ConnectionTracker | undefined\n #closing: Promise<void> | undefined\n\n constructor(options: HonoAdapterOptions) {\n this.app = options.app ?? new Hono()\n this.#serveOptions = options.serveOptions ?? {}\n }\n\n readonly fetch = (request: Request, env?: unknown): Response | Promise<Response> => {\n this.#prepare()\n return this.app.fetch(request, env)\n }\n\n registerRoute(route: AdapterRoute): void {\n const handler = route.handler\n const path = toHonoPath(route.path)\n this.#routes++\n if (route.method === 'HEAD') {\n // Hono routes HEAD requests to GET handlers: serve them from a GET handler that only answers HEAD\n this.app.get(path, (c: Context, next: Next) =>\n c.req.raw.method === 'HEAD' ? this.#serve(c, handler) : next().then(() => c.res),\n )\n return\n }\n this.app.on(route.method as HonoMethod, path, (c: Context) => this.#serve(c, handler))\n }\n\n setFallbackHandler(handler: FallbackHandler): void {\n this.#fallback = handler\n }\n\n #prepare(): void {\n if (this.#prepared) return\n this.#prepared = true\n this.app.notFound(c => this.#serveFallback(c, undefined))\n this.app.onError((error, c) => this.#serveFallback(c, error))\n }\n\n async #serve(c: Context, handler: AdapterHandler): Promise<Response> {\n const request = adapterRequest(c)\n // Hono routes the WHATWG-normalised URL (`/a/../b` → `/b`, `%2e%2e` resolved): when the raw\n // target differs (Node.js), let the plugin route the raw path, like on the other adapters\n const reply =\n this.#fallback !== undefined && pathOf(request.url) !== pathOf(pathAndQuery(c.req.raw.url))\n ? await this.#fallback(request)\n : await handler(request)\n return this.#toResponse(c, reply)\n }\n\n async #serveFallback(c: Context, error: unknown): Promise<Response> {\n if (this.#fallback === undefined) throw error ?? new Error('No fallback handler')\n return this.#toResponse(c, await this.#fallback(adapterRequest(c), error))\n }\n\n /**\n * Whether `app` has routes the adapter did not register (middleware, other routes): only they can\n * set headers on the context or read the response.\n */\n #middleware(): boolean {\n return this.app.routes.length !== this.#routes\n }\n\n #toResponse(c: Context, reply: AdapterReply): Response {\n const middleware = this.#middleware()\n const outgoing = nodeOutgoing(c)\n if (outgoing !== undefined) {\n const body = reply.body instanceof ReadableStream ? reply.body : null\n if (outgoing.destroyed) {\n // @hono/node-server: the client left before the reply was ready (an async pre-flight). The\n // server would neither read nor cancel the body: release it (event source, request scope)\n discardStream(body)\n return toResponse(c, { ...reply, body: null }, middleware)\n }\n if (body !== null) {\n // the client may leave before @hono/node-server reads the body (its own listener then comes\n // too late): a body nobody locked is cancelled when the response closes unfinished\n outgoing.once('close', () => {\n if (!outgoing.writableFinished) discardStream(body)\n })\n }\n // a quiet event stream must outlive a socket timeout\n if (reply.eventStream === true && typeof outgoing.setTimeout === 'function') outgoing.setTimeout(0)\n }\n if (this.#bun === undefined || !(reply.body instanceof ReadableStream)) return toResponse(c, reply, middleware)\n // served by Bun.serve(): observe the end of the body (idle timeout, native blob: see #bunFetch)\n const reader = reply.body.getReader()\n let ended = false\n let onEnd = (): void => undefined\n const end = (): void => {\n if (ended) return\n ended = true\n onEnd()\n }\n const body = new ReadableStream<Uint8Array>(\n {\n async pull(controller) {\n let result: Awaited<ReturnType<typeof reader.read>>\n try {\n result = await reader.read()\n } catch (error) {\n end()\n throw error\n }\n if (result.done) {\n end() // before closing: Bun ignores timeout() once the response is complete\n controller.close()\n return\n }\n controller.enqueue(result.value)\n },\n cancel(reason) {\n end()\n return reader.cancel(reason)\n },\n },\n { highWaterMark: 0 },\n )\n BUN_BODIES.set(body, {\n blob: reply.blob,\n onEnd: callback => {\n onEnd = callback\n },\n })\n return toResponse(c, { ...reply, body }, middleware)\n }\n\n /** The fetch handler given to `Bun.serve()`. */\n async #bunFetch(request: Request, server: BunServer, idleTimeout: number): Promise<Response> {\n const response = await this.app.fetch(request, server)\n const body = response.body\n const served = body === null ? undefined : BUN_BODIES.get(body)\n if (body === null || served === undefined) return response\n BUN_BODIES.delete(body)\n if (served.blob !== undefined && !body.locked) {\n // a Blob is sent by Bun itself (sendfile, exact Content-Length; a stream would be chunked). Bun\n // opens its file as soon as it takes the response: release the stream (and with it the request\n // scope, which may delete the file) right after\n const native = new Response(served.blob, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n })\n setImmediate(() => discardStream(body))\n return native\n }\n if (idleTimeout > 0 && typeof server.timeout === 'function') {\n // the idle timeout of Bun.serve() also cuts a response whose stream pauses (SSE, slow producers):\n // lift it while the stream is sent, then restore it for the keep-alive connection\n server.timeout(request, 0)\n served.onEnd(() => {\n try {\n server.timeout?.(request, idleTimeout)\n } catch {\n // the connection is gone\n }\n })\n }\n return response\n }\n\n async listen(port: number, host: string): Promise<HttpServerAddress> {\n this.#prepare()\n this.#closing = undefined\n const bun = (globalThis as { Bun?: BunRuntime }).Bun\n if (isBun() && bun !== undefined) {\n const options = this.#serveOptions\n const idleTimeout = typeof options.idleTimeout === 'number' ? options.idleTimeout : BUN_IDLE_TIMEOUT\n const server = bun.serve({\n // Znight enforces the body limits (route @BodyLimit, plugin bodyLimit): not Bun's 128 MiB default\n maxRequestBodySize: Number.MAX_SAFE_INTEGER,\n ...options,\n port,\n hostname: host,\n fetch: (request, server) => this.#bunFetch(request, server, idleTimeout),\n })\n this.#bun = server\n return serverAddress(server.hostname, server.port)\n }\n let nodeServer: typeof import('@hono/node-server')\n try {\n nodeServer = await import('@hono/node-server')\n } catch (error) {\n throw new Error(\n 'honoAdapter on Node.js requires the optional peer dependency \"@hono/node-server\" (npm i @hono/node-server)',\n { cause: error },\n )\n }\n const server = nodeServer.createAdaptorServer({ fetch: this.app.fetch }) as Server\n this.#node = server\n this.#tracker = new ConnectionTracker(server)\n await new Promise<void>((resolve, reject) => {\n const onError = (error: Error): void => reject(error)\n server.once('error', onError)\n server.listen(port, host, () => {\n server.off('error', onError)\n resolve()\n })\n })\n const address = server.address()\n if (address === null || typeof address === 'string') return serverAddress(host, port)\n return serverAddress(address.address, address.port, address.family)\n }\n\n close(options: AdapterCloseOptions = {}): Promise<void> {\n const bun = this.#bun\n if (bun !== undefined) {\n if (options.force === true) {\n this.#closing = Promise.resolve(bun.stop(true))\n } else {\n this.#closing ??= Promise.resolve(bun.stop(false))\n }\n return this.#closing\n }\n const server = this.#node\n const tracker = this.#tracker\n if (server === undefined || tracker === undefined) return Promise.resolve()\n this.#closing = closeServer(server, tracker, this.#closing, options.force === true)\n return this.#closing\n }\n\n inject(request: InjectRequest): Promise<InjectResponse> {\n return bufferInjectStream(stream => this.injectStream(stream), request)\n }\n\n injectStream(request: InjectStreamRequest): Promise<InjectStreamResponse> {\n this.#prepare()\n return injectFetchStream(request => this.app.request(request, undefined, INJECT_ENV), request)\n }\n}\n\nfunction adapterRequest(c: Context): AdapterRequest {\n return new HonoAdapterRequest(c)\n}\n\n/** A Hono request as an {@link AdapterRequest} (a class: no closure per request). */\nclass HonoAdapterRequest implements AdapterRequest {\n readonly method: string\n readonly url: string\n readonly headers: Readonly<Record<string, string | undefined>>\n readonly ip: string | undefined\n readonly remoteAddress: string | undefined\n readonly encrypted: boolean\n readonly raw: Request\n readonly rawResponse: Context\n /** The Node.js request of `@hono/node-server`, absent on Bun and for `inject()`. */\n readonly #incoming: NodeIncoming | undefined\n /**\n * The same request over HTTP/1.1, read directly (Node.js streams): no Fetch `Request` and body\n * stream are built for it.\n */\n readonly #http1: IncomingMessage | undefined\n #body: Promise<Uint8Array> | undefined\n\n constructor(c: Context) {\n const request = c.req.raw\n const incoming = nodeIncoming(c)\n const http1 = incoming !== undefined && incoming.httpVersionMajor < 2 ? (incoming as IncomingMessage) : undefined\n const address = remoteAddress(c)\n this.method = request.method\n // the raw target on Node.js; on Bun only the normalised URL is known\n this.url = pathAndQuery(typeof incoming?.url === 'string' && incoming.url !== '' ? incoming.url : request.url)\n this.headers = (http1 === undefined ? undefined : distinctHeaders(http1)) ?? fetchHeaders(request.headers)\n this.ip = address\n this.remoteAddress = address\n this.encrypted = incoming === undefined ? request.url.startsWith('https:') : incoming.socket?.encrypted === true\n this.raw = request\n this.rawResponse = c\n this.#incoming = incoming\n this.#http1 = http1\n }\n\n readBody(limit: number): Promise<Uint8Array> {\n if (this.#body === undefined) {\n const http1 = this.#http1\n this.#body =\n http1 === undefined\n ? readWebStream(() => this.raw.body, limit, this.headers['content-length'])\n : readNodeBody(http1, limit)\n }\n return this.#body\n }\n\n streamBody(limit: number): ReadableStream<Uint8Array> {\n const http1 = this.#http1\n if (http1 !== undefined) return nodeBodyStream(http1, limit)\n return webBodyStream(() => this.raw.body, limit, this.headers['content-length'])\n }\n\n // asked only without content-length / transfer-encoding: a request read from an HTTP/1.1\n // connection then has no body (`@hono/node-server` still gives every POST a body stream), an\n // HTTP/2 one unless its HEADERS frame ended the stream; a Web `Request` (Bun, inject) says it\n hasBody(): boolean {\n const incoming = this.#incoming\n return incoming === undefined\n ? this.raw.body !== null\n : incoming.httpVersionMajor >= 2 && incoming.stream?.endAfterHeaders === false\n }\n\n onAbort(listener: () => void): void {\n const outgoing = nodeOutgoing(this.rawResponse)\n if (outgoing !== undefined) {\n onResponseAbort(outgoing as unknown as ServerResponse, listener)\n return\n }\n // Bun.serve() and inject(): the request signal aborts when the client leaves\n const signal = this.raw.signal\n if (signal.aborted) queueMicrotask(listener)\n else signal.addEventListener('abort', listener, { once: true })\n }\n}\n\n/** The headers of a Fetch request: lower-case names, repeated headers joined (`Headers` semantics). */\nfunction fetchHeaders(source: Headers): Record<string, string> {\n const headers: Record<string, string> = {}\n for (const [name, value] of source) headers[name] = value\n return headers\n}\n\n/**\n * The headers of a Node.js request when none is repeated: then they hold the values of its Fetch\n * `Headers` (Node.js keeps the first of some repeated headers where `Headers` join them all).\n */\nfunction distinctHeaders(request: IncomingMessage): Record<string, string | undefined> | undefined {\n const headers = request.headers\n let count = 0\n for (const name in headers) {\n if (typeof headers[name] !== 'string') return undefined\n count++\n }\n return count * 2 === request.rawHeaders.length ? (headers as Record<string, string | undefined>) : undefined\n}\n\n/** The part of a Node.js `ServerResponse` used here. */\ninterface NodeOutgoing {\n readonly writableFinished: boolean\n readonly destroyed: boolean\n once(event: 'close', listener: () => void): unknown\n setTimeout?(ms: number): unknown\n}\n\n/** The Node.js response of `@hono/node-server` (`c.env.outgoing`), absent on Bun and for `inject()`. */\nfunction nodeOutgoing(c: Context): NodeOutgoing | undefined {\n const outgoing = (c.env as { outgoing?: Partial<NodeOutgoing> } | undefined)?.outgoing\n return typeof outgoing?.once === 'function' ? (outgoing as NodeOutgoing) : undefined\n}\n\n/** The Node.js request of `@hono/node-server` (`c.env.incoming`), absent on Bun and for `inject()`. */\nfunction nodeIncoming(c: Context): NodeIncoming | undefined {\n const incoming = (c.env as { incoming?: Partial<NodeIncoming> } | undefined)?.incoming\n return typeof incoming?.httpVersionMajor === 'number' ? (incoming as NodeIncoming) : undefined\n}\n\nfunction pathAndQuery(url: string): string {\n if (url.startsWith('/')) return url\n const scheme = url.indexOf('://')\n const start = url.indexOf('/', scheme === -1 ? 0 : scheme + 3)\n return start === -1 ? '/' : url.slice(start)\n}\n\nfunction pathOf(url: string): string {\n const query = url.indexOf('?')\n return query === -1 ? url : url.slice(0, query)\n}\n\nfunction remoteAddress(c: Context): string | undefined {\n const env = c.env as\n | { incoming?: { socket?: { remoteAddress?: string } }; requestIP?: BunServer['requestIP'] }\n | undefined\n if (env === undefined || env === null) return undefined\n if (typeof env.requestIP === 'function') return env.requestIP(c.req.raw)?.address ?? undefined\n return env.incoming?.socket?.remoteAddress\n}\n\n/**\n * The `Response` of a reply. Without `middleware` (see `#middleware`), nothing set headers on the\n * context and nothing reads the response: headers given as a record of strings are sent as is by\n * `@hono/node-server` (no `Headers` object), and a text body as a string.\n */\nfunction toResponse(c: Context, reply: AdapterReply, middleware: boolean): Response {\n if (reply.raw === true) {\n discardStream(reply.body instanceof ReadableStream ? reply.body : null)\n return c.res\n }\n if (!middleware && isStringRecord(reply.headers)) {\n return new Response(reply.text ?? reply.body, {\n status: reply.status,\n headers: reply.headers as Readonly<Record<string, string>>,\n })\n }\n const headers = new Headers()\n try {\n for (const name of Object.keys(reply.headers)) {\n const value = reply.headers[name]\n if (typeof value === 'string') headers.set(name, value)\n else if (value !== undefined) for (const item of value) headers.append(name, item)\n }\n } catch {\n // a header Headers refuses (the plugin validates them): never rethrow into Hono's onError, which\n // would run the pipeline again; release the body and answer a plain 500, like writeNodeReply\n discardStream(reply.body instanceof ReadableStream ? reply.body : null)\n return new Response('{\"statusCode\":500,\"error\":\"Internal Server Error\"}', {\n status: 500,\n headers: { 'content-type': 'application/json; charset=utf-8' },\n })\n }\n if (middleware) keepContextCookies(c, headers)\n return new Response(reply.text ?? reply.body, { status: reply.status, headers })\n}\n\n/** Whether every header has a single value (a record of strings is a valid `HeadersInit`). */\nfunction isStringRecord(headers: Readonly<Record<string, HeaderValue>>): boolean {\n for (const name in headers) if (typeof headers[name] !== 'string') return false\n return true\n}\n\n/**\n * Keeps the cookies Hono middleware set before the Znight route (`setCookie(c, ...)`,\n * `c.header('set-cookie', ..., { append: true })`) before the reply's own, like the other adapters.\n * Hono drops the headers of the context when a handler returns its own `Response` (or, when a\n * middleware read `c.res`, replaces the response's cookies with the context's): the reply's cookies\n * are added to the context too, so that both ways end with every cookie, in order.\n */\nfunction keepContextCookies(c: Context, headers: Headers): void {\n let context: string[]\n try {\n // the headers of the context without changing it (`c.res` would create the context response)\n context = c.newResponse(null).headers.getSetCookie()\n } catch {\n return // a status a bodyless Response refuses (`c.status(101)`): nothing to keep\n }\n if (context.length === 0) return\n const own = headers.getSetCookie()\n for (const line of own) c.header('set-cookie', line, { append: true })\n headers.delete('set-cookie')\n for (const line of context) headers.append('set-cookie', line)\n for (const line of own) headers.append('set-cookie', line)\n}\n"],"mappings":"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"}
|