@daloyjs/core 0.35.0 → 0.35.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/README.md +145 -29
- package/dist/adapters/bun.d.ts +0 -1
- package/dist/adapters/bun.js +0 -1
- package/dist/adapters/cloudflare.d.ts +0 -1
- package/dist/adapters/cloudflare.js +0 -1
- package/dist/adapters/deno.d.ts +0 -1
- package/dist/adapters/deno.js +0 -1
- package/dist/adapters/fastly.d.ts +0 -1
- package/dist/adapters/fastly.js +0 -1
- package/dist/adapters/lambda.d.ts +0 -1
- package/dist/adapters/lambda.js +0 -1
- package/dist/adapters/node.d.ts +11 -1
- package/dist/adapters/node.js +246 -44
- package/dist/adapters/vercel.d.ts +0 -1
- package/dist/adapters/vercel.js +0 -1
- package/dist/app.d.ts +52 -1
- package/dist/app.js +557 -146
- package/dist/banner.d.ts +0 -1
- package/dist/banner.js +0 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -1
- package/dist/client.d.ts +0 -1
- package/dist/client.js +0 -1
- package/dist/combine.d.ts +0 -1
- package/dist/combine.js +0 -1
- package/dist/compression.d.ts +0 -1
- package/dist/compression.js +0 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/conn-info.d.ts +0 -1
- package/dist/conn-info.js +0 -1
- package/dist/contract.d.ts +0 -1
- package/dist/contract.js +0 -1
- package/dist/cookie.d.ts +0 -1
- package/dist/cookie.js +0 -1
- package/dist/dependency.d.ts +0 -1
- package/dist/dependency.js +0 -1
- package/dist/discriminator.d.ts +0 -1
- package/dist/discriminator.js +0 -1
- package/dist/docs.d.ts +0 -1
- package/dist/docs.js +0 -1
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js +6 -3
- package/dist/etag.d.ts +0 -1
- package/dist/etag.js +0 -1
- package/dist/fetch-guard.d.ts +0 -1
- package/dist/fetch-guard.js +0 -1
- package/dist/hashing.d.ts +0 -1
- package/dist/hashing.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/ip-restriction.d.ts +0 -1
- package/dist/ip-restriction.js +0 -1
- package/dist/jwk.d.ts +0 -1
- package/dist/jwk.js +0 -1
- package/dist/jwt.d.ts +0 -1
- package/dist/jwt.js +0 -1
- package/dist/load-shedding.d.ts +0 -1
- package/dist/load-shedding.js +0 -1
- package/dist/logger.d.ts +0 -1
- package/dist/logger.js +0 -1
- package/dist/middleware.d.ts +0 -1
- package/dist/middleware.js +16 -13
- package/dist/multipart.d.ts +0 -1
- package/dist/multipart.js +0 -1
- package/dist/openapi.d.ts +0 -1
- package/dist/openapi.js +0 -1
- package/dist/rate-limit-redis.d.ts +0 -1
- package/dist/rate-limit-redis.js +0 -1
- package/dist/router.d.ts +0 -1
- package/dist/router.js +0 -1
- package/dist/safe-redirect.d.ts +2 -3
- package/dist/safe-redirect.js +2 -3
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/schema.d.ts +0 -1
- package/dist/schema.js +0 -1
- package/dist/security-schemes.d.ts +0 -1
- package/dist/security-schemes.js +0 -1
- package/dist/security.d.ts +0 -16
- package/dist/security.js +32 -4
- package/dist/session.d.ts +0 -1
- package/dist/session.js +0 -1
- package/dist/streaming.d.ts +0 -1
- package/dist/streaming.js +0 -1
- package/dist/subdomains.d.ts +0 -1
- package/dist/subdomains.js +0 -1
- package/dist/time-claims.d.ts +0 -1
- package/dist/time-claims.js +0 -1
- package/dist/tracing.d.ts +0 -1
- package/dist/tracing.js +0 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/websocket.d.ts +0 -1
- package/dist/websocket.js +0 -1
- package/package.json +6 -2
- package/dist/adapters/bun.d.ts.map +0 -1
- package/dist/adapters/bun.js.map +0 -1
- package/dist/adapters/cloudflare.d.ts.map +0 -1
- package/dist/adapters/cloudflare.js.map +0 -1
- package/dist/adapters/deno.d.ts.map +0 -1
- package/dist/adapters/deno.js.map +0 -1
- package/dist/adapters/fastly.d.ts.map +0 -1
- package/dist/adapters/fastly.js.map +0 -1
- package/dist/adapters/lambda.d.ts.map +0 -1
- package/dist/adapters/lambda.js.map +0 -1
- package/dist/adapters/node.d.ts.map +0 -1
- package/dist/adapters/node.js.map +0 -1
- package/dist/adapters/vercel.d.ts.map +0 -1
- package/dist/adapters/vercel.js.map +0 -1
- package/dist/app.d.ts.map +0 -1
- package/dist/app.js.map +0 -1
- package/dist/banner.d.ts.map +0 -1
- package/dist/banner.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/combine.d.ts.map +0 -1
- package/dist/combine.js.map +0 -1
- package/dist/compression.d.ts.map +0 -1
- package/dist/compression.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/conn-info.d.ts.map +0 -1
- package/dist/conn-info.js.map +0 -1
- package/dist/contract.d.ts.map +0 -1
- package/dist/contract.js.map +0 -1
- package/dist/cookie.d.ts.map +0 -1
- package/dist/cookie.js.map +0 -1
- package/dist/dependency.d.ts.map +0 -1
- package/dist/dependency.js.map +0 -1
- package/dist/discriminator.d.ts.map +0 -1
- package/dist/discriminator.js.map +0 -1
- package/dist/docs.d.ts.map +0 -1
- package/dist/docs.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/etag.d.ts.map +0 -1
- package/dist/etag.js.map +0 -1
- package/dist/fetch-guard.d.ts.map +0 -1
- package/dist/fetch-guard.js.map +0 -1
- package/dist/hashing.d.ts.map +0 -1
- package/dist/hashing.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/ip-restriction.d.ts.map +0 -1
- package/dist/ip-restriction.js.map +0 -1
- package/dist/jwk.d.ts.map +0 -1
- package/dist/jwk.js.map +0 -1
- package/dist/jwt.d.ts.map +0 -1
- package/dist/jwt.js.map +0 -1
- package/dist/load-shedding.d.ts.map +0 -1
- package/dist/load-shedding.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js.map +0 -1
- package/dist/multipart.d.ts.map +0 -1
- package/dist/multipart.js.map +0 -1
- package/dist/openapi.d.ts.map +0 -1
- package/dist/openapi.js.map +0 -1
- package/dist/rate-limit-redis.d.ts.map +0 -1
- package/dist/rate-limit-redis.js.map +0 -1
- package/dist/router.d.ts.map +0 -1
- package/dist/router.js.map +0 -1
- package/dist/safe-redirect.d.ts.map +0 -1
- package/dist/safe-redirect.js.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js.map +0 -1
- package/dist/security-schemes.d.ts.map +0 -1
- package/dist/security-schemes.js.map +0 -1
- package/dist/security.d.ts.map +0 -1
- package/dist/security.js.map +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js.map +0 -1
- package/dist/streaming.d.ts.map +0 -1
- package/dist/streaming.js.map +0 -1
- package/dist/subdomains.d.ts.map +0 -1
- package/dist/subdomains.js.map +0 -1
- package/dist/time-claims.d.ts.map +0 -1
- package/dist/time-claims.js.map +0 -1
- package/dist/tracing.d.ts.map +0 -1
- package/dist/tracing.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/websocket.d.ts.map +0 -1
- package/dist/websocket.js.map +0 -1
package/dist/adapters/node.js
CHANGED
|
@@ -4,30 +4,33 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createServer, } from "node:http";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
|
+
import { DALOY_RAW_BODY, DALOY_RAW_STREAM, DALOY_REQUEST_RAW_BODY } from "../app.js";
|
|
7
8
|
import { FrameSink, encodeFrame, encodeClosePayload, encodeSendPayload, validateUpgrade, validateSelectedSubprotocol, checkWebSocketOrigin, WS_OPCODE, WS_CLOSE_CODE, WS_READY_STATE, WS_MAX_CONTROL_PAYLOAD, WebSocketProtocolError, WebSocketPayloadTooLargeError, } from "../websocket.js";
|
|
8
9
|
/** Start a Node.js HTTP (and optional WebSocket) server bound to the given {@link App}. */
|
|
9
10
|
export function serve(app, opts = {}) {
|
|
10
11
|
const trustProxy = opts.trustProxy === true;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const bufferedBodyMaxBytes = typeof opts.bufferedBodyMaxBytes === "number" && opts.bufferedBodyMaxBytes >= 0
|
|
13
|
+
? opts.bufferedBodyMaxBytes
|
|
14
|
+
: DEFAULT_BUFFERED_BODY_MAX_BYTES;
|
|
15
|
+
const server = createServer({ maxHeaderSize: opts.maxHeaderBytes ?? 16 * 1024 }, (req, res) => {
|
|
16
|
+
// GET/HEAD: no body work, dispatch directly. Keep this first so the GET
|
|
17
|
+
// hot path doesn't pay for any of the buffering bookkeeping below.
|
|
18
|
+
const method = req.method;
|
|
19
|
+
if (method === "GET" || method === "HEAD" || method === undefined) {
|
|
20
|
+
dispatchToApp(app, req, res, trustProxy, undefined);
|
|
21
|
+
return;
|
|
16
22
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
res.destroy(e);
|
|
29
|
-
}
|
|
23
|
+
// POST/PUT/PATCH/DELETE with a small known content-length: pre-buffer
|
|
24
|
+
// bytes from the Node socket directly so the Request constructor gets a
|
|
25
|
+
// Uint8Array body instead of `Readable.toWeb(req)`. This skips the
|
|
26
|
+
// WHATWG-stream adapter that dominates POST throughput on Node.
|
|
27
|
+
const cl = req.headers["content-length"];
|
|
28
|
+
const n = cl ? Number(cl) : NaN;
|
|
29
|
+
if (Number.isFinite(n) && n >= 0 && n <= bufferedBodyMaxBytes) {
|
|
30
|
+
bufferRequestBody(req, n).then((bytes) => dispatchToApp(app, req, res, trustProxy, bytes), (e) => writeAdapterError(res, e));
|
|
31
|
+
return;
|
|
30
32
|
}
|
|
33
|
+
dispatchToApp(app, req, res, trustProxy, undefined);
|
|
31
34
|
});
|
|
32
35
|
server.requestTimeout = opts.connectionTimeoutMs ?? 30_000;
|
|
33
36
|
server.headersTimeout = opts.connectionTimeoutMs ?? 30_000;
|
|
@@ -69,33 +72,176 @@ export function serve(app, opts = {}) {
|
|
|
69
72
|
}
|
|
70
73
|
return { server, port, close };
|
|
71
74
|
}
|
|
72
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Default pre-buffer ceiling for the Node adapter. 256 KiB is a compromise:
|
|
77
|
+
* large enough that the vast majority of JSON / form requests stay on the
|
|
78
|
+
* fast (Uint8Array) path, small enough that N concurrent in-flight bodies
|
|
79
|
+
* don't pin huge amounts of memory. Override via
|
|
80
|
+
* {@link NodeServerOptions.bufferedBodyMaxBytes}. The actual security cap
|
|
81
|
+
* on body size remains `App.bodyLimitBytes`.
|
|
82
|
+
*/
|
|
83
|
+
const DEFAULT_BUFFERED_BODY_MAX_BYTES = 256 * 1024;
|
|
84
|
+
function dispatchToApp(app, req, res, trustProxy, bufferedBody) {
|
|
85
|
+
let request;
|
|
86
|
+
try {
|
|
87
|
+
request = toWebRequest(req, trustProxy, bufferedBody);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
writeAdapterError(res, e);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const responseOrPromise = app.fetch(request);
|
|
94
|
+
if (responseOrPromise instanceof Promise) {
|
|
95
|
+
responseOrPromise.then((response) => {
|
|
96
|
+
try {
|
|
97
|
+
const sent = sendWebResponse(response, res);
|
|
98
|
+
if (sent instanceof Promise)
|
|
99
|
+
sent.catch((e) => writeAdapterError(res, e));
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
writeAdapterError(res, e);
|
|
103
|
+
}
|
|
104
|
+
}, (e) => writeAdapterError(res, e));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
try {
|
|
108
|
+
const sent = sendWebResponse(responseOrPromise, res);
|
|
109
|
+
if (sent instanceof Promise)
|
|
110
|
+
sent.catch((e) => writeAdapterError(res, e));
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
writeAdapterError(res, e);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function bufferRequestBody(req, expected) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
// Pre-allocate to the declared Content-Length. The caller has already
|
|
120
|
+
// checked `expected <= BUFFERED_BODY_MAX_BYTES` (1 MiB) so this
|
|
121
|
+
// allocation is bounded and DoS-safe. Skipping the intermediate
|
|
122
|
+
// `chunks: Buffer[]` array + `Buffer.concat` avoids one full-body
|
|
123
|
+
// copy per request — significant at 1 MiB bodies under load.
|
|
124
|
+
// Use `Buffer.alloc` (zero-filled) rather than `Buffer.allocUnsafe`:
|
|
125
|
+
// the unsafe variant returns uninitialized memory and is forbidden by
|
|
126
|
+
// `verify:no-unsafe-buffer`. Any unwritten tail is sliced off below.
|
|
127
|
+
const out = expected > 0 ? Buffer.alloc(expected) : null;
|
|
128
|
+
let received = 0;
|
|
129
|
+
let settled = false;
|
|
130
|
+
const onData = (chunk) => {
|
|
131
|
+
if (settled)
|
|
132
|
+
return;
|
|
133
|
+
const next = received + chunk.length;
|
|
134
|
+
if (next > expected) {
|
|
135
|
+
settled = true;
|
|
136
|
+
cleanup();
|
|
137
|
+
req.destroy();
|
|
138
|
+
reject(new Error("Request body exceeded declared Content-Length"));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// out is non-null here because next > 0 implies expected > 0.
|
|
142
|
+
chunk.copy(out, received);
|
|
143
|
+
received = next;
|
|
144
|
+
};
|
|
145
|
+
const onEnd = () => {
|
|
146
|
+
if (settled)
|
|
147
|
+
return;
|
|
148
|
+
settled = true;
|
|
149
|
+
cleanup();
|
|
150
|
+
if (received === 0) {
|
|
151
|
+
resolve(new Uint8Array(0));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Trust Content-Length: if the client under-delivered we still
|
|
155
|
+
// resolve the prefix actually received (matches prior behavior).
|
|
156
|
+
const buf = received === expected ? out : out.subarray(0, received);
|
|
157
|
+
resolve(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
158
|
+
};
|
|
159
|
+
const onErr = (err) => {
|
|
160
|
+
if (settled)
|
|
161
|
+
return;
|
|
162
|
+
settled = true;
|
|
163
|
+
cleanup();
|
|
164
|
+
reject(err);
|
|
165
|
+
};
|
|
166
|
+
const cleanup = () => {
|
|
167
|
+
req.off("data", onData);
|
|
168
|
+
req.off("end", onEnd);
|
|
169
|
+
req.off("error", onErr);
|
|
170
|
+
req.off("aborted", onErr);
|
|
171
|
+
};
|
|
172
|
+
req.on("data", onData);
|
|
173
|
+
req.on("end", onEnd);
|
|
174
|
+
req.on("error", onErr);
|
|
175
|
+
req.on("aborted", onErr);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function writeAdapterError(res, e) {
|
|
179
|
+
if (!res.headersSent) {
|
|
180
|
+
res.statusCode = 500;
|
|
181
|
+
res.setHeader("content-type", "application/problem+json");
|
|
182
|
+
res.end(JSON.stringify({
|
|
183
|
+
type: "https://daloyjs.dev/errors/internal",
|
|
184
|
+
title: "Internal Server Error",
|
|
185
|
+
status: 500,
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
res.destroy(e);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function toWebRequest(req, trustProxy, bufferedBody) {
|
|
193
|
+
const reqHeaders = req.headers;
|
|
73
194
|
const forwardedHost = trustProxy
|
|
74
|
-
? firstHeader(
|
|
195
|
+
? firstHeader(reqHeaders["x-forwarded-host"])
|
|
75
196
|
: undefined;
|
|
76
|
-
const host = forwardedHost ??
|
|
197
|
+
const host = forwardedHost ?? reqHeaders.host ?? "localhost";
|
|
77
198
|
const forwardedProto = trustProxy
|
|
78
|
-
? firstHeader(
|
|
199
|
+
? firstHeader(reqHeaders["x-forwarded-proto"])
|
|
79
200
|
: undefined;
|
|
80
201
|
const proto = forwardedProto ??
|
|
81
202
|
(req.socket.encrypted ? "https" : "http");
|
|
82
203
|
const url = `${proto}://${host}${req.url ?? "/"}`;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
204
|
+
// Build headers from `rawHeaders` (a flat [k0,v0,k1,v1,...] array) instead
|
|
205
|
+
// of the parsed `req.headers` object. This matches @hono/node-server's
|
|
206
|
+
// `newHeadersFromIncoming`: one `new Headers([[k,v],...])` constructor
|
|
207
|
+
// call rather than N `headers.set()` calls. It is also stricter for
|
|
208
|
+
// duplicate-Host smuggling — Node coalesces some singleton headers down
|
|
209
|
+
// to the first value on `req.headers`, but `rawHeaders` preserves every
|
|
210
|
+
// occurrence so `assertNoDuplicateSingletonHeaders` actually sees them.
|
|
211
|
+
// Skip HTTP/2 pseudo-headers (leading ':') defensively, even though
|
|
212
|
+
// node:http's createServer is HTTP/1.1 only today.
|
|
213
|
+
const rawHeaders = req.rawHeaders;
|
|
214
|
+
const headerPairs = [];
|
|
215
|
+
for (let i = 0; i < rawHeaders.length; i += 2) {
|
|
216
|
+
const k = rawHeaders[i];
|
|
217
|
+
if (k.charCodeAt(0) === 58 /* ':' */)
|
|
86
218
|
continue;
|
|
87
|
-
|
|
88
|
-
headers.set(k, v.join(", "));
|
|
89
|
-
else
|
|
90
|
-
headers.set(k, String(v));
|
|
219
|
+
headerPairs.push([k, rawHeaders[i + 1]]);
|
|
91
220
|
}
|
|
221
|
+
const headers = new Headers(headerPairs);
|
|
92
222
|
const method = req.method ?? "GET";
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
223
|
+
if (method === "GET" || method === "HEAD") {
|
|
224
|
+
return new Request(url, { method, headers });
|
|
225
|
+
}
|
|
226
|
+
if (bufferedBody !== undefined) {
|
|
227
|
+
const req2 = new Request(url, {
|
|
228
|
+
method,
|
|
229
|
+
headers,
|
|
230
|
+
body: bufferedBody,
|
|
231
|
+
});
|
|
232
|
+
// Stash the validated bytes so readBodyLimited (and any other internal
|
|
233
|
+
// body reader) can skip the WHATWG ReadableStream reader loop. The
|
|
234
|
+
// adapter has already enforced BUFFERED_BODY_MAX_BYTES + Content-Length
|
|
235
|
+
// here; readBodyLimited re-checks against the caller's limit.
|
|
236
|
+
req2[DALOY_REQUEST_RAW_BODY] = bufferedBody;
|
|
237
|
+
return req2;
|
|
97
238
|
}
|
|
98
|
-
return new Request(url,
|
|
239
|
+
return new Request(url, {
|
|
240
|
+
method,
|
|
241
|
+
headers,
|
|
242
|
+
body: Readable.toWeb(req),
|
|
243
|
+
duplex: "half",
|
|
244
|
+
});
|
|
99
245
|
}
|
|
100
246
|
function firstHeader(v) {
|
|
101
247
|
if (v === undefined)
|
|
@@ -106,22 +252,79 @@ function firstHeader(v) {
|
|
|
106
252
|
const comma = raw.indexOf(",");
|
|
107
253
|
return (comma === -1 ? raw : raw.slice(0, comma)).trim() || undefined;
|
|
108
254
|
}
|
|
109
|
-
|
|
255
|
+
function sendWebResponse(res, out) {
|
|
110
256
|
out.statusCode = res.status;
|
|
111
257
|
res.headers.forEach((v, k) => out.setHeader(k, v));
|
|
258
|
+
// Fast-path: response was produced by serializeResult and carries the raw
|
|
259
|
+
// body bytes via the DALOY_RAW_BODY Symbol. Skip arrayBuffer() and the
|
|
260
|
+
// reader-loop microtask churn entirely for buffer-backed responses.
|
|
261
|
+
const raw = res[DALOY_RAW_BODY];
|
|
262
|
+
if (raw !== undefined) {
|
|
263
|
+
if (raw === null) {
|
|
264
|
+
out.end();
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
out.end(raw);
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Fast-path: handler returned a raw stream. Check before `!res.body` —
|
|
272
|
+
// a Node Readable is stashed alongside a null Response body, and the
|
|
273
|
+
// `new Response(null)` constructor also auto-sets `content-length: 0`
|
|
274
|
+
// which we must strip so Node falls back to chunked transfer-encoding.
|
|
275
|
+
const rawStream = res[DALOY_RAW_STREAM];
|
|
276
|
+
if (rawStream !== undefined) {
|
|
277
|
+
if (typeof rawStream.pipe === "function" && !(rawStream instanceof ReadableStream)) {
|
|
278
|
+
// Node `Readable` from the handler: skip the Web-stream bridge entirely
|
|
279
|
+
// and `.pipe(out)` like Fastify/Koa/Express do.
|
|
280
|
+
out.removeHeader("content-length");
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const r = rawStream;
|
|
283
|
+
const onError = (err) => {
|
|
284
|
+
r.destroy();
|
|
285
|
+
reject(err);
|
|
286
|
+
};
|
|
287
|
+
r.once("error", onError);
|
|
288
|
+
out.once("error", onError);
|
|
289
|
+
out.once("finish", () => resolve());
|
|
290
|
+
r.pipe(out);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return pumpBody(rawStream, out);
|
|
294
|
+
}
|
|
112
295
|
if (!res.body) {
|
|
113
296
|
out.end();
|
|
114
297
|
return;
|
|
115
298
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
299
|
+
const ct = out.getHeader("content-type");
|
|
300
|
+
const isStream = ct && typeof ct === "string" && ct.startsWith("text/event-stream");
|
|
301
|
+
const rawLength = out.getHeader("content-length");
|
|
302
|
+
const contentLength = typeof rawLength === "number"
|
|
303
|
+
? rawLength
|
|
304
|
+
: typeof rawLength === "string"
|
|
305
|
+
? Number(rawLength)
|
|
306
|
+
: Number.NaN;
|
|
307
|
+
if (!isStream && Number.isFinite(contentLength) && contentLength <= 64 * 1024) {
|
|
308
|
+
return res.arrayBuffer().then((ab) => {
|
|
309
|
+
out.end(Buffer.from(ab));
|
|
310
|
+
});
|
|
123
311
|
}
|
|
124
|
-
out
|
|
312
|
+
return pumpBody(res.body, out);
|
|
313
|
+
}
|
|
314
|
+
function pumpBody(body, out) {
|
|
315
|
+
// Delegate to Node's native pipe: it honors backpressure and avoids the
|
|
316
|
+
// per-chunk microtask overhead of an explicit `await reader.read()` loop.
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
const readable = Readable.fromWeb(body);
|
|
319
|
+
const onError = (err) => {
|
|
320
|
+
readable.destroy();
|
|
321
|
+
reject(err);
|
|
322
|
+
};
|
|
323
|
+
readable.once("error", onError);
|
|
324
|
+
out.once("error", onError);
|
|
325
|
+
out.once("finish", () => resolve());
|
|
326
|
+
readable.pipe(out);
|
|
327
|
+
});
|
|
125
328
|
}
|
|
126
329
|
// ---------- WebSocket upgrade ----------
|
|
127
330
|
async function handleUpgrade(app, req, socket, head, trustProxy) {
|
|
@@ -390,4 +593,3 @@ class NodeWebSocketConnection {
|
|
|
390
593
|
}
|
|
391
594
|
}
|
|
392
595
|
}
|
|
393
|
-
//# sourceMappingURL=node.js.map
|
package/dist/adapters/vercel.js
CHANGED
package/dist/app.d.ts
CHANGED
|
@@ -471,6 +471,34 @@ export interface IntrospectedRoute {
|
|
|
471
471
|
*/
|
|
472
472
|
meta?: import("./types.js").RouteMeta;
|
|
473
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Internal Symbol used to stash the raw Uint8Array (or null) body on a
|
|
476
|
+
* `Response` produced by {@link serializeResult}. Adapters that want to
|
|
477
|
+
* avoid the cost of `await response.arrayBuffer()` for buffer-backed
|
|
478
|
+
* responses can read this property and write the bytes directly. The
|
|
479
|
+
* Symbol is intentionally module-private (not exported) to keep this an
|
|
480
|
+
* implementation detail — userland code should never depend on it.
|
|
481
|
+
*/
|
|
482
|
+
export declare const DALOY_RAW_BODY: unique symbol;
|
|
483
|
+
/**
|
|
484
|
+
* Internal Symbol used by adapters to stash a pre-buffered request body on
|
|
485
|
+
* the `Request` instance. When set, {@link readBodyLimited} skips the
|
|
486
|
+
* `ReadableStream` reader loop and returns the cached bytes directly after
|
|
487
|
+
* re-checking them against the caller-supplied limit. Adapters MUST only
|
|
488
|
+
* attach bytes they have already validated against the configured
|
|
489
|
+
* {@link AppOptions.bodyLimitBytes}; the limit re-check in
|
|
490
|
+
* `readBodyLimited` is defense-in-depth, not the primary cap. Module-public
|
|
491
|
+
* so first-party adapters can opt in; not part of the userland API surface.
|
|
492
|
+
*/
|
|
493
|
+
export declare const DALOY_REQUEST_RAW_BODY: unique symbol;
|
|
494
|
+
/**
|
|
495
|
+
* Internal Symbol set by handlers/serializers to attach a raw stream
|
|
496
|
+
* (Node `Readable` or Web `ReadableStream`) to a `Response`. The Node
|
|
497
|
+
* adapter pipes the stream straight to the socket, skipping the
|
|
498
|
+
* Web-stream reader bridge. Module-public so first-party adapters can
|
|
499
|
+
* opt in; userland code should not depend on it.
|
|
500
|
+
*/
|
|
501
|
+
export declare const DALOY_RAW_STREAM: unique symbol;
|
|
474
502
|
/**
|
|
475
503
|
* Contract-first HTTP application.
|
|
476
504
|
*
|
|
@@ -526,6 +554,14 @@ export declare class App {
|
|
|
526
554
|
/** Public registry: enables OpenAPI gen, typed-client gen, dead-route detection. */
|
|
527
555
|
readonly routes: RouteDefinition<any, any, any, any>[];
|
|
528
556
|
private router;
|
|
557
|
+
/**
|
|
558
|
+
* Memoized result of `isProduction()`. The inputs (`options.env`,
|
|
559
|
+
* `options.production`, `process.env.NODE_ENV`) cannot change between
|
|
560
|
+
* the moment a route is dispatched and the moment its error is rendered,
|
|
561
|
+
* so reading `process.env.NODE_ENV` on every error response is wasted
|
|
562
|
+
* work in the hot path. Computed lazily on first read.
|
|
563
|
+
*/
|
|
564
|
+
private _productionCache;
|
|
529
565
|
/** WebSocket route registry. Adapters look up handlers via `app.webSocketRoutes.find()`. */
|
|
530
566
|
readonly webSocketRoutes: WebSocketRegistry;
|
|
531
567
|
private prefix;
|
|
@@ -536,6 +572,13 @@ export declare class App {
|
|
|
536
572
|
private routeSecurityMarkers;
|
|
537
573
|
/** Decorator bag merged into ctx.state on every request. */
|
|
538
574
|
private decorations;
|
|
575
|
+
/**
|
|
576
|
+
* Count of own keys on {@link decorations}. Tracked alongside the bag so the
|
|
577
|
+
* dispatch hot path can take a `count === 0` fast path and skip the
|
|
578
|
+
* `Object.assign` spread on the common case (no `app.decorate()` calls).
|
|
579
|
+
* Updated only when {@link decorate} mutates the bag.
|
|
580
|
+
*/
|
|
581
|
+
private decorationsCount;
|
|
539
582
|
private installedPlugins;
|
|
540
583
|
private closeHooks;
|
|
541
584
|
private closeHooksRun;
|
|
@@ -570,6 +613,15 @@ export declare class App {
|
|
|
570
613
|
* dashboards see the misconfiguration without flooding on every retry.
|
|
571
614
|
*/
|
|
572
615
|
private trustProxyWarned;
|
|
616
|
+
/**
|
|
617
|
+
* Cached merge of `options.hooks` only. Used on the cold 404/405 path
|
|
618
|
+
* and as the baseline for cross-origin guard decisions when no route
|
|
619
|
+
* matches.
|
|
620
|
+
*/
|
|
621
|
+
private _globalHooksCache;
|
|
622
|
+
private _globalCorsAllowsCache;
|
|
623
|
+
private get globalHooks();
|
|
624
|
+
private get globalCorsAllows();
|
|
573
625
|
constructor(options?: AppOptions);
|
|
574
626
|
/**
|
|
575
627
|
* Validate {@link AppOptions.disconnectStatusCode}.
|
|
@@ -1058,4 +1110,3 @@ export declare function createApp(options?: AppOptions): App;
|
|
|
1058
1110
|
* @internal
|
|
1059
1111
|
*/
|
|
1060
1112
|
export declare function _resetPackageJsonCacheForTests(): void;
|
|
1061
|
-
//# sourceMappingURL=app.d.ts.map
|