@ailura/nestjs-hono-adapter 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +411 -0
- package/dist/body.d.ts +35 -0
- package/dist/body.d.ts.map +1 -0
- package/dist/body.js +180 -0
- package/dist/body.js.map +1 -0
- package/dist/bridge.d.ts +64 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +168 -0
- package/dist/bridge.js.map +1 -0
- package/dist/closing.d.ts +13 -0
- package/dist/closing.d.ts.map +1 -0
- package/dist/closing.js +30 -0
- package/dist/closing.js.map +1 -0
- package/dist/context.d.ts +22 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +2 -0
- package/dist/context.js.map +1 -0
- package/dist/cors-middleware.d.ts +64 -0
- package/dist/cors-middleware.d.ts.map +1 -0
- package/dist/cors-middleware.js +211 -0
- package/dist/cors-middleware.js.map +1 -0
- package/dist/handler-bridge.d.ts +51 -0
- package/dist/handler-bridge.d.ts.map +1 -0
- package/dist/handler-bridge.js +122 -0
- package/dist/handler-bridge.js.map +1 -0
- package/dist/hono-lifecycle.d.ts +90 -0
- package/dist/hono-lifecycle.d.ts.map +1 -0
- package/dist/hono-lifecycle.js +169 -0
- package/dist/hono-lifecycle.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/path.d.ts +3 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +144 -0
- package/dist/path.js.map +1 -0
- package/dist/query.d.ts +19 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +238 -0
- package/dist/query.js.map +1 -0
- package/dist/response-helpers.d.ts +16 -0
- package/dist/response-helpers.d.ts.map +1 -0
- package/dist/response-helpers.js +45 -0
- package/dist/response-helpers.js.map +1 -0
- package/dist/response-writer.d.ts +28 -0
- package/dist/response-writer.d.ts.map +1 -0
- package/dist/response-writer.js +52 -0
- package/dist/response-writer.js.map +1 -0
- package/dist/route-adapter.d.ts +53 -0
- package/dist/route-adapter.d.ts.map +1 -0
- package/dist/route-adapter.js +141 -0
- package/dist/route-adapter.js.map +1 -0
- package/dist/server-adapter.d.ts +106 -0
- package/dist/server-adapter.d.ts.map +1 -0
- package/dist/server-adapter.js +153 -0
- package/dist/server-adapter.js.map +1 -0
- package/dist/sse.d.ts +67 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +211 -0
- package/dist/sse.js.map +1 -0
- package/dist/static-assets.d.ts +39 -0
- package/dist/static-assets.d.ts.map +1 -0
- package/dist/static-assets.js +155 -0
- package/dist/static-assets.js.map +1 -0
- package/dist/version-filter.d.ts +24 -0
- package/dist/version-filter.d.ts.map +1 -0
- package/dist/version-filter.js +107 -0
- package/dist/version-filter.js.map +1 -0
- package/dist/versioned-route.d.ts +21 -0
- package/dist/versioned-route.d.ts.map +1 -0
- package/dist/versioned-route.js +15 -0
- package/dist/versioned-route.js.map +1 -0
- package/dist/views.d.ts +42 -0
- package/dist/views.d.ts.map +1 -0
- package/dist/views.js +110 -0
- package/dist/views.js.map +1 -0
- package/dist/ws-adapter.d.ts +81 -0
- package/dist/ws-adapter.d.ts.map +1 -0
- package/dist/ws-adapter.js +214 -0
- package/dist/ws-adapter.js.map +1 -0
- package/dist/ws-client.d.ts +68 -0
- package/dist/ws-client.d.ts.map +1 -0
- package/dist/ws-client.js +135 -0
- package/dist/ws-client.js.map +1 -0
- package/dist/ws-server.d.ts +23 -0
- package/dist/ws-server.d.ts.map +1 -0
- package/dist/ws-server.js +37 -0
- package/dist/ws-server.js.map +1 -0
- package/dist/ws.d.ts +14 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +12 -0
- package/dist/ws.js.map +1 -0
- package/package.json +99 -0
- package/src/body.ts +251 -0
- package/src/bridge.ts +308 -0
- package/src/closing.ts +38 -0
- package/src/context.ts +25 -0
- package/src/cors-middleware.ts +347 -0
- package/src/handler-bridge.ts +226 -0
- package/src/hono-lifecycle.ts +259 -0
- package/src/index.ts +27 -0
- package/src/path.ts +169 -0
- package/src/query.ts +304 -0
- package/src/response-helpers.ts +60 -0
- package/src/response-writer.ts +100 -0
- package/src/route-adapter.ts +261 -0
- package/src/server-adapter.ts +294 -0
- package/src/sse.ts +274 -0
- package/src/static-assets.ts +247 -0
- package/src/version-filter.ts +170 -0
- package/src/versioned-route.ts +30 -0
- package/src/views.ts +188 -0
- package/src/ws-adapter.ts +329 -0
- package/src/ws-client.ts +190 -0
- package/src/ws-server.ts +40 -0
- package/src/ws.ts +13 -0
package/src/body.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BadRequestException,
|
|
5
|
+
PayloadTooLargeException,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
|
|
8
|
+
import type { NestContext } from './context.ts';
|
|
9
|
+
import { parseQuery } from './query.ts';
|
|
10
|
+
|
|
11
|
+
/** Methods that never carry a payload. */
|
|
12
|
+
const BODYLESS_METHODS = new Set(['GET', 'HEAD']);
|
|
13
|
+
|
|
14
|
+
/** The media types that carry a JSON document. */
|
|
15
|
+
const JSON_TYPE = /^application\/(?:.+\+)?json$/u;
|
|
16
|
+
|
|
17
|
+
/** How a limit may be written, in the notation Nest accepts. */
|
|
18
|
+
const LIMIT =
|
|
19
|
+
/^\s*(?<amount>\d+(?:\.\d+)?)\s*(?<unit>b|kb|mb|gb|tb|pb)?\s*$/iu;
|
|
20
|
+
|
|
21
|
+
/** The size a kibibyte stands for, which every unit is built on. */
|
|
22
|
+
const BYTES_PER_KIB = 1024;
|
|
23
|
+
|
|
24
|
+
const BYTES_PER_MIB = BYTES_PER_KIB * BYTES_PER_KIB;
|
|
25
|
+
const BYTES_PER_GIB = BYTES_PER_MIB * BYTES_PER_KIB;
|
|
26
|
+
const BYTES_PER_TIB = BYTES_PER_GIB * BYTES_PER_KIB;
|
|
27
|
+
const BYTES_PER_PIB = BYTES_PER_TIB * BYTES_PER_KIB;
|
|
28
|
+
|
|
29
|
+
/** The size each unit of a limit stands for. */
|
|
30
|
+
const UNITS = new Map<string, number>([
|
|
31
|
+
['b', 1],
|
|
32
|
+
['gb', BYTES_PER_GIB],
|
|
33
|
+
['kb', BYTES_PER_KIB],
|
|
34
|
+
['mb', BYTES_PER_MIB],
|
|
35
|
+
['pb', BYTES_PER_PIB],
|
|
36
|
+
['tb', BYTES_PER_TIB],
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A payload as the pipeline reads it: the parsed value, the
|
|
41
|
+
* uploaded files when the request was multipart, and the bytes
|
|
42
|
+
* when the application asked to keep them.
|
|
43
|
+
*/
|
|
44
|
+
interface ParsedBody {
|
|
45
|
+
body: unknown;
|
|
46
|
+
files: Record<string, unknown> | undefined;
|
|
47
|
+
rawBody: Buffer | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface BodyOptions {
|
|
51
|
+
readonly bodyLimit: number | undefined;
|
|
52
|
+
readonly rawBody: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A multipart payload, split into its two halves. */
|
|
56
|
+
interface FormParts {
|
|
57
|
+
readonly fields: Record<string, unknown>;
|
|
58
|
+
readonly files: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reads a size the way Nest's own parsers do, so a limit such
|
|
63
|
+
* as `100kb` means the same thing here as it does on Express.
|
|
64
|
+
*/
|
|
65
|
+
function toByteLimit(limit: number | string): number {
|
|
66
|
+
if (typeof limit === 'number') {
|
|
67
|
+
return limit;
|
|
68
|
+
}
|
|
69
|
+
const match = LIMIT.exec(limit);
|
|
70
|
+
if (match === null) {
|
|
71
|
+
throw new TypeError(
|
|
72
|
+
`The body limit "${limit}" is not a size such as ` +
|
|
73
|
+
'"100kb" or "1mb".',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const { amount, unit } = match.groups ?? {};
|
|
77
|
+
const bytes = Number(amount ?? '');
|
|
78
|
+
const suffix = (unit ?? 'b').toLowerCase();
|
|
79
|
+
return Math.floor(bytes * (UNITS.get(suffix) ?? 1));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function mediaType(context: NestContext): string {
|
|
83
|
+
const header = context.req.header('content-type') ?? '';
|
|
84
|
+
return (header.split(';')[0] ?? '').trim().toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function assertWithinLength(
|
|
88
|
+
context: NestContext,
|
|
89
|
+
limit: number | undefined,
|
|
90
|
+
): void {
|
|
91
|
+
if (limit === undefined) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const declared = context.req.header('content-length');
|
|
95
|
+
if (declared === undefined) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const length = Number(declared);
|
|
99
|
+
if (Number.isFinite(length) && length > limit) {
|
|
100
|
+
throw new PayloadTooLargeException();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertWithinSize(
|
|
105
|
+
size: number,
|
|
106
|
+
limit: number | undefined,
|
|
107
|
+
): void {
|
|
108
|
+
if (limit !== undefined && size > limit) {
|
|
109
|
+
throw new PayloadTooLargeException();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function readBytes(
|
|
114
|
+
context: NestContext,
|
|
115
|
+
limit: number | undefined,
|
|
116
|
+
): Promise<Uint8Array> {
|
|
117
|
+
const bytes = new Uint8Array(await context.req.arrayBuffer());
|
|
118
|
+
assertWithinSize(bytes.byteLength, limit);
|
|
119
|
+
return bytes;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function textOf(bytes: Uint8Array): string {
|
|
123
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseJson(bytes: Uint8Array): unknown {
|
|
127
|
+
const text = textOf(bytes).trim();
|
|
128
|
+
if (text === '') {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(text);
|
|
133
|
+
} catch {
|
|
134
|
+
throw new BadRequestException('Malformed request body');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function parseBytes(bytes: Uint8Array, type: string): unknown {
|
|
139
|
+
if (JSON_TYPE.test(type)) {
|
|
140
|
+
return parseJson(bytes);
|
|
141
|
+
}
|
|
142
|
+
if (type === 'application/x-www-form-urlencoded') {
|
|
143
|
+
return parseQuery(textOf(bytes));
|
|
144
|
+
}
|
|
145
|
+
if (type.startsWith('text/')) {
|
|
146
|
+
return textOf(bytes);
|
|
147
|
+
}
|
|
148
|
+
if (bytes.byteLength === 0) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return Buffer.from(bytes);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function readForm(
|
|
155
|
+
context: NestContext,
|
|
156
|
+
): Promise<Record<string, string | File>> {
|
|
157
|
+
try {
|
|
158
|
+
return await context.req.parseBody();
|
|
159
|
+
} catch {
|
|
160
|
+
throw new BadRequestException('Malformed request body');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function splitFields(
|
|
165
|
+
form: Record<string, string | File>,
|
|
166
|
+
): FormParts {
|
|
167
|
+
const fields: Record<string, unknown> = {};
|
|
168
|
+
const files: Record<string, unknown> = {};
|
|
169
|
+
for (const [name, value] of Object.entries(form)) {
|
|
170
|
+
if (typeof value === 'string') {
|
|
171
|
+
fields[name] = value;
|
|
172
|
+
} else {
|
|
173
|
+
files[name] = value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { fields, files };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Reads a multipart request into the two halves a controller
|
|
181
|
+
* expects: the text fields in the body, the uploads in `files`.
|
|
182
|
+
* The bytes are never buffered, so `rawBody` stays empty.
|
|
183
|
+
*/
|
|
184
|
+
async function readMultipart(
|
|
185
|
+
context: NestContext,
|
|
186
|
+
): Promise<ParsedBody> {
|
|
187
|
+
const form = await readForm(context);
|
|
188
|
+
const parts = splitFields(form);
|
|
189
|
+
const parsed: ParsedBody = {
|
|
190
|
+
body: parts.fields,
|
|
191
|
+
files: undefined,
|
|
192
|
+
rawBody: undefined,
|
|
193
|
+
};
|
|
194
|
+
if (Object.keys(parts.files).length > 0) {
|
|
195
|
+
parsed.files = parts.files;
|
|
196
|
+
}
|
|
197
|
+
return parsed;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function emptyBody(): ParsedBody {
|
|
201
|
+
return {
|
|
202
|
+
body: undefined,
|
|
203
|
+
files: undefined,
|
|
204
|
+
rawBody: undefined,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function readPayload(
|
|
209
|
+
context: NestContext,
|
|
210
|
+
type: string,
|
|
211
|
+
options: BodyOptions,
|
|
212
|
+
): Promise<ParsedBody> {
|
|
213
|
+
const bytes = await readBytes(context, options.bodyLimit);
|
|
214
|
+
const parsed: ParsedBody = {
|
|
215
|
+
body: parseBytes(bytes, type),
|
|
216
|
+
files: undefined,
|
|
217
|
+
rawBody: undefined,
|
|
218
|
+
};
|
|
219
|
+
if (options.rawBody) {
|
|
220
|
+
parsed.rawBody = Buffer.from(bytes);
|
|
221
|
+
}
|
|
222
|
+
return parsed;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Reads the payload for the pipeline.
|
|
227
|
+
*
|
|
228
|
+
* On Express and Fastify Nest installs parser middleware that
|
|
229
|
+
* fills `req.body` before the pipeline runs. Hono parses on
|
|
230
|
+
* demand, so the adapter reads the body here and hands it over
|
|
231
|
+
* in the same place. A payload that does not match its content
|
|
232
|
+
* type is refused with the exception Nest raises for a failed
|
|
233
|
+
* parse, so filters and logging see it like any other failure.
|
|
234
|
+
*/
|
|
235
|
+
function readBody(
|
|
236
|
+
context: NestContext,
|
|
237
|
+
options: BodyOptions,
|
|
238
|
+
): Promise<ParsedBody> {
|
|
239
|
+
if (BODYLESS_METHODS.has(context.req.method)) {
|
|
240
|
+
return Promise.resolve(emptyBody());
|
|
241
|
+
}
|
|
242
|
+
const type = mediaType(context);
|
|
243
|
+
assertWithinLength(context, options.bodyLimit);
|
|
244
|
+
if (type.startsWith('multipart/form-data')) {
|
|
245
|
+
return readMultipart(context);
|
|
246
|
+
}
|
|
247
|
+
return readPayload(context, type, options);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export { readBody, toByteLimit };
|
|
251
|
+
export type { ParsedBody };
|
package/src/bridge.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
|
|
4
|
+
import { HttpStatus, StreamableFile } from '@nestjs/common';
|
|
5
|
+
|
|
6
|
+
import type { NestContext } from './context.ts';
|
|
7
|
+
import { parseQuery } from './query.ts';
|
|
8
|
+
import type { ParsedQuery } from './query.ts';
|
|
9
|
+
|
|
10
|
+
const JSON_CONTENT_TYPE = 'application/json; charset=UTF-8';
|
|
11
|
+
const TEXT_CONTENT_TYPE = 'text/plain; charset=UTF-8';
|
|
12
|
+
|
|
13
|
+
/** The header a proxy sets with the protocol it received. */
|
|
14
|
+
const FORWARDED_PROTO = 'x-forwarded-proto';
|
|
15
|
+
|
|
16
|
+
/** The header a proxy sets with the host it received. */
|
|
17
|
+
const FORWARDED_HOST = 'x-forwarded-host';
|
|
18
|
+
|
|
19
|
+
/** The header a proxy sets with the address it received from. */
|
|
20
|
+
const FORWARDED_FOR = 'x-forwarded-for';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The request properties Nest's core reads. Every property is
|
|
24
|
+
* declared, and the ones that can legitimately be absent are
|
|
25
|
+
* written as `| undefined`, so `exactOptionalPropertyTypes`
|
|
26
|
+
* cannot hide a field that was never populated.
|
|
27
|
+
*/
|
|
28
|
+
interface NestRequest {
|
|
29
|
+
method: string;
|
|
30
|
+
url: string;
|
|
31
|
+
originalUrl: string;
|
|
32
|
+
path: string;
|
|
33
|
+
hostname: string;
|
|
34
|
+
protocol: string;
|
|
35
|
+
secure: boolean;
|
|
36
|
+
ip: string | undefined;
|
|
37
|
+
ips: string[];
|
|
38
|
+
headers: Record<string, string>;
|
|
39
|
+
query: ParsedQuery;
|
|
40
|
+
params: Record<string, string>;
|
|
41
|
+
hosts: Record<string, string>;
|
|
42
|
+
socket: IncomingMessage['socket'];
|
|
43
|
+
raw: IncomingMessage;
|
|
44
|
+
body: unknown;
|
|
45
|
+
rawBody: Buffer | undefined;
|
|
46
|
+
session: unknown;
|
|
47
|
+
files: Record<string, unknown> | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Continues to whatever handles the request next. The adapter
|
|
52
|
+
* contract types it as returning `void` while Hono returns a
|
|
53
|
+
* promise, so it is kept as `unknown` and the result ignored.
|
|
54
|
+
*/
|
|
55
|
+
type NextHandler = () => unknown;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A handler as Nest registers it: the route proxy, a middleware
|
|
59
|
+
* or the not-found proxy all share this shape.
|
|
60
|
+
*/
|
|
61
|
+
type NestHandler = (
|
|
62
|
+
request: NestRequest,
|
|
63
|
+
response: NestContext,
|
|
64
|
+
next: NextHandler,
|
|
65
|
+
) => unknown;
|
|
66
|
+
|
|
67
|
+
/** How much of a proxy's word the deployment believes. */
|
|
68
|
+
interface RequestOptions {
|
|
69
|
+
readonly trustProxy: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What the forwarded headers of one request said. */
|
|
73
|
+
interface Forwarded {
|
|
74
|
+
readonly addresses: string[];
|
|
75
|
+
readonly hosts: string[];
|
|
76
|
+
readonly protocols: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isPrimitive(
|
|
80
|
+
body: unknown,
|
|
81
|
+
): body is string | number | boolean | bigint {
|
|
82
|
+
const kind = typeof body;
|
|
83
|
+
return (
|
|
84
|
+
kind === 'string' ||
|
|
85
|
+
kind === 'number' ||
|
|
86
|
+
kind === 'boolean' ||
|
|
87
|
+
kind === 'bigint'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isBinaryBody(
|
|
92
|
+
body: unknown,
|
|
93
|
+
): body is Uint8Array | ArrayBuffer | ReadableStream {
|
|
94
|
+
return (
|
|
95
|
+
body instanceof Uint8Array ||
|
|
96
|
+
body instanceof ArrayBuffer ||
|
|
97
|
+
body instanceof ReadableStream
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reads one forwarded header as the list it is: a proxy may
|
|
103
|
+
* append to it, and the first entry is the one closest to the
|
|
104
|
+
* client.
|
|
105
|
+
*/
|
|
106
|
+
function forwarded(
|
|
107
|
+
context: NestContext,
|
|
108
|
+
name: string,
|
|
109
|
+
): string[] {
|
|
110
|
+
const header = context.req.header(name);
|
|
111
|
+
if (header === undefined) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
return header
|
|
115
|
+
.split(',')
|
|
116
|
+
.map((value) => value.trim())
|
|
117
|
+
.filter((value) => value !== '');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Reads a forwarded header only when the deployment says a
|
|
122
|
+
* proxy sets it: a client that can write it can claim any
|
|
123
|
+
* address it likes.
|
|
124
|
+
*/
|
|
125
|
+
function trustedValues(
|
|
126
|
+
context: NestContext,
|
|
127
|
+
name: string,
|
|
128
|
+
trustProxy: boolean,
|
|
129
|
+
): string[] {
|
|
130
|
+
if (!trustProxy) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
return forwarded(context, name);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function forwardedValues(
|
|
137
|
+
context: NestContext,
|
|
138
|
+
options: RequestOptions,
|
|
139
|
+
): Forwarded {
|
|
140
|
+
const { trustProxy } = options;
|
|
141
|
+
return {
|
|
142
|
+
addresses: trustedValues(
|
|
143
|
+
context,
|
|
144
|
+
FORWARDED_FOR,
|
|
145
|
+
trustProxy,
|
|
146
|
+
),
|
|
147
|
+
hosts: trustedValues(context, FORWARDED_HOST, trustProxy),
|
|
148
|
+
protocols: trustedValues(
|
|
149
|
+
context,
|
|
150
|
+
FORWARDED_PROTO,
|
|
151
|
+
trustProxy,
|
|
152
|
+
),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Maps a Hono context onto the request object Nest expects.
|
|
158
|
+
*
|
|
159
|
+
* Nest reads the request as a bag of properties rather than
|
|
160
|
+
* through an interface, so this is the single place where the
|
|
161
|
+
* Web request is translated into that bag.
|
|
162
|
+
*/
|
|
163
|
+
function toNestRequest(
|
|
164
|
+
context: NestContext,
|
|
165
|
+
options: RequestOptions,
|
|
166
|
+
): NestRequest {
|
|
167
|
+
const target = new URL(context.req.url);
|
|
168
|
+
const route = `${target.pathname}${target.search}`;
|
|
169
|
+
const { incoming } = context.env;
|
|
170
|
+
const values = forwardedValues(context, options);
|
|
171
|
+
const [forwardedProtocol] = values.protocols;
|
|
172
|
+
const scheme =
|
|
173
|
+
forwardedProtocol ?? target.protocol.replace(':', '');
|
|
174
|
+
const [host] = values.hosts;
|
|
175
|
+
const [address] = values.addresses;
|
|
176
|
+
return {
|
|
177
|
+
body: undefined,
|
|
178
|
+
files: undefined,
|
|
179
|
+
headers: context.req.header(),
|
|
180
|
+
hostname: host ?? target.hostname,
|
|
181
|
+
hosts: {},
|
|
182
|
+
ip: address ?? incoming.socket.remoteAddress,
|
|
183
|
+
ips: values.addresses,
|
|
184
|
+
method: context.req.method,
|
|
185
|
+
originalUrl: route,
|
|
186
|
+
params: context.req.param(),
|
|
187
|
+
path: target.pathname,
|
|
188
|
+
protocol: scheme,
|
|
189
|
+
query: parseQuery(target.search),
|
|
190
|
+
raw: incoming,
|
|
191
|
+
rawBody: undefined,
|
|
192
|
+
secure: scheme === 'https',
|
|
193
|
+
session: undefined,
|
|
194
|
+
socket: incoming.socket,
|
|
195
|
+
url: route,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function toTextResponse(
|
|
200
|
+
body: string | number | boolean | bigint,
|
|
201
|
+
status: number,
|
|
202
|
+
headers: Headers,
|
|
203
|
+
): Response {
|
|
204
|
+
if (!headers.has('content-type')) {
|
|
205
|
+
headers.set('content-type', TEXT_CONTENT_TYPE);
|
|
206
|
+
}
|
|
207
|
+
return new Response(String(body), { headers, status });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function toList(
|
|
211
|
+
value: string | readonly string[],
|
|
212
|
+
): readonly string[] {
|
|
213
|
+
if (typeof value === 'string') {
|
|
214
|
+
return [value];
|
|
215
|
+
}
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Writes the disposition a file declares. Nest accepts a list,
|
|
221
|
+
* which Node treats as one header line per entry, so each entry
|
|
222
|
+
* is appended rather than overwritten.
|
|
223
|
+
*/
|
|
224
|
+
function setDisposition(
|
|
225
|
+
headers: Headers,
|
|
226
|
+
disposition: string | readonly string[],
|
|
227
|
+
): void {
|
|
228
|
+
for (const value of toList(disposition)) {
|
|
229
|
+
headers.append('content-disposition', value);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Streams a file Nest built, keeping the headers it declares.
|
|
235
|
+
* `StreamableFile` is the one response type that reaches the
|
|
236
|
+
* adapter as a Node stream, so it is converted here rather than
|
|
237
|
+
* buffered.
|
|
238
|
+
*/
|
|
239
|
+
function toFileResponse(
|
|
240
|
+
file: StreamableFile,
|
|
241
|
+
status: number,
|
|
242
|
+
headers: Headers,
|
|
243
|
+
): Response {
|
|
244
|
+
const { type, disposition, length } = file.getHeaders();
|
|
245
|
+
if (type !== '') {
|
|
246
|
+
headers.set('content-type', type);
|
|
247
|
+
}
|
|
248
|
+
if (disposition !== undefined) {
|
|
249
|
+
setDisposition(headers, disposition);
|
|
250
|
+
}
|
|
251
|
+
if (length !== undefined) {
|
|
252
|
+
headers.set('content-length', String(length));
|
|
253
|
+
}
|
|
254
|
+
return new Response(Readable.toWeb(file.getStream()), {
|
|
255
|
+
headers,
|
|
256
|
+
status,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function toResponse(
|
|
261
|
+
body: unknown,
|
|
262
|
+
status: number,
|
|
263
|
+
headers: Headers,
|
|
264
|
+
): Response {
|
|
265
|
+
if (body === undefined || body === null) {
|
|
266
|
+
return new Response(undefined, { headers, status });
|
|
267
|
+
}
|
|
268
|
+
if (body instanceof StreamableFile) {
|
|
269
|
+
return toFileResponse(body, status, headers);
|
|
270
|
+
}
|
|
271
|
+
if (isBinaryBody(body)) {
|
|
272
|
+
return new Response(body, { headers, status });
|
|
273
|
+
}
|
|
274
|
+
if (isPrimitive(body)) {
|
|
275
|
+
return toTextResponse(body, status, headers);
|
|
276
|
+
}
|
|
277
|
+
headers.set('content-type', JSON_CONTENT_TYPE);
|
|
278
|
+
return Response.json(body, { headers, status });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Builds the Web response for a value Nest returned.
|
|
283
|
+
*
|
|
284
|
+
* The headers already recorded on the context are copied first:
|
|
285
|
+
* `@Header()` reaches the adapter through `setHeader()`, which
|
|
286
|
+
* stores them on the context, and replacing the response would
|
|
287
|
+
* otherwise drop them.
|
|
288
|
+
*/
|
|
289
|
+
function buildResponse(
|
|
290
|
+
context: NestContext,
|
|
291
|
+
body: unknown,
|
|
292
|
+
status: number = HttpStatus.OK,
|
|
293
|
+
): Response {
|
|
294
|
+
return toResponse(
|
|
295
|
+
body,
|
|
296
|
+
status,
|
|
297
|
+
new Headers(context.res.headers),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export {
|
|
302
|
+
buildResponse,
|
|
303
|
+
toNestRequest,
|
|
304
|
+
type NestHandler,
|
|
305
|
+
type NestRequest,
|
|
306
|
+
type NextHandler,
|
|
307
|
+
type RequestOptions,
|
|
308
|
+
};
|
package/src/closing.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { HttpStatus } from '@nestjs/common';
|
|
2
|
+
import type { MiddlewareHandler } from 'hono';
|
|
3
|
+
|
|
4
|
+
import type { NodeEnv } from './context.ts';
|
|
5
|
+
|
|
6
|
+
/** The answer Nest's own adapters give while shutting down. */
|
|
7
|
+
const CLOSING_BODY = {
|
|
8
|
+
message: 'Service Unavailable',
|
|
9
|
+
statusCode: HttpStatus.SERVICE_UNAVAILABLE,
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
/** The answer a request that arrives while closing receives. */
|
|
13
|
+
function closingResponse(): Response {
|
|
14
|
+
return Response.json(CLOSING_BODY, {
|
|
15
|
+
status: HttpStatus.SERVICE_UNAVAILABLE,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Answers the requests that arrive while the server is closing.
|
|
21
|
+
*
|
|
22
|
+
* A request already in flight has passed this step and is left
|
|
23
|
+
* to finish; one that arrives afterwards is refused instead of
|
|
24
|
+
* being accepted and then dropped when the socket goes away.
|
|
25
|
+
* The step does nothing unless the application asked for it.
|
|
26
|
+
*/
|
|
27
|
+
function closingBridge(
|
|
28
|
+
isClosing: () => boolean,
|
|
29
|
+
): MiddlewareHandler<NodeEnv> {
|
|
30
|
+
return (_context, next) => {
|
|
31
|
+
if (isClosing()) {
|
|
32
|
+
return Promise.resolve(closingResponse());
|
|
33
|
+
}
|
|
34
|
+
return next();
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { closingBridge };
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HttpBindings } from '@hono/node-server';
|
|
2
|
+
import type { Context, Hono } from 'hono';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Environment `@hono/node-server` attaches to every request. It
|
|
6
|
+
* carries the raw Node request and response, which is how the
|
|
7
|
+
* adapter reaches the client socket without depending on them
|
|
8
|
+
* anywhere else.
|
|
9
|
+
*/
|
|
10
|
+
interface NodeEnv {
|
|
11
|
+
Bindings: HttpBindings;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The Hono application, typed with its Node bindings. */
|
|
15
|
+
type NestHono = Hono<NodeEnv>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A Hono context. It doubles as the response object Nest writes
|
|
19
|
+
* to: keeping the transport in a Web `Response` is what makes
|
|
20
|
+
* the adapter independent of the platform the request arrived
|
|
21
|
+
* on.
|
|
22
|
+
*/
|
|
23
|
+
type NestContext = Context<NodeEnv>;
|
|
24
|
+
|
|
25
|
+
export type { NestContext, NestHono, NodeEnv };
|