@juspay/neurolink 13.8.11 → 13.8.12
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/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +410 -410
- package/dist/cli/commands/proxy.js +16 -1
- package/dist/proxy/requestBodyLimit.d.ts +60 -0
- package/dist/proxy/requestBodyLimit.js +97 -0
- package/dist/server/adapters/honoAdapter.d.ts +13 -0
- package/dist/server/adapters/honoAdapter.js +73 -83
- package/dist/server/utils/rawBody.d.ts +17 -0
- package/dist/server/utils/rawBody.js +69 -4
- package/docs-site/static/search-index.json +3 -1
- package/package.json +11 -4
|
@@ -13,6 +13,8 @@ import { ensureTelemetry, getLangfuseHealthStatus, } from "../../services/server
|
|
|
13
13
|
import { logger } from "../../utils/logger.js";
|
|
14
14
|
import { applyAllClients, applyClientsOnProxyStart, restoreAllClients, } from "../proxy-clients/registry.js";
|
|
15
15
|
import { resolveProxyConfigPath } from "../../proxy/proxyConfig.js";
|
|
16
|
+
import { readProxyRequestText } from "../../proxy/requestBodyLimit.js";
|
|
17
|
+
import { RawBodyTooLargeError } from "../../server/utils/rawBody.js";
|
|
16
18
|
import { redactUrlsInText, sanitizeForLog, } from "../../utils/logSanitize.js";
|
|
17
19
|
import { raceWithAbort, withTimeout, } from "../../utils/async/withTimeout.js";
|
|
18
20
|
import { startProxyHttpTrace } from "../../proxy/proxyTracer.js";
|
|
@@ -1511,6 +1513,13 @@ export async function createProxyStartApp(params) {
|
|
|
1511
1513
|
}),
|
|
1512
1514
|
]);
|
|
1513
1515
|
};
|
|
1516
|
+
/** 413 in the wire format this proxy answers its other request errors in. */
|
|
1517
|
+
const respondRequestTooLarge = async (c, metadata, message) => {
|
|
1518
|
+
if (metadata) {
|
|
1519
|
+
await recordRuntimeError(metadata, 413, "request_too_large", message);
|
|
1520
|
+
}
|
|
1521
|
+
return c.json({ type: "error", error: { type: "request_too_large", message } }, 413);
|
|
1522
|
+
};
|
|
1514
1523
|
app.onError(async (err, c) => {
|
|
1515
1524
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1516
1525
|
logger.always(`[proxy] unhandled error: ${errMsg}`);
|
|
@@ -1703,7 +1712,13 @@ export async function createProxyStartApp(params) {
|
|
|
1703
1712
|
let body;
|
|
1704
1713
|
let rawBody;
|
|
1705
1714
|
if (method === "post") {
|
|
1706
|
-
|
|
1715
|
+
// Bounded before it is parsed, and so before the share gate below
|
|
1716
|
+
// (which needs the model), the way the server adapters bound theirs.
|
|
1717
|
+
const read = await readProxyRequestText(c.req.raw, route.path);
|
|
1718
|
+
if (read instanceof RawBodyTooLargeError) {
|
|
1719
|
+
return respondRequestTooLarge(c, metadata, read.message);
|
|
1720
|
+
}
|
|
1721
|
+
rawBody = read;
|
|
1707
1722
|
try {
|
|
1708
1723
|
body = rawBody ? JSON.parse(rawBody) : emptyBody;
|
|
1709
1724
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-body ceiling for the proxy's POST routes.
|
|
3
|
+
*
|
|
4
|
+
* The body is read before anything else about the request is known (the model,
|
|
5
|
+
* the share token), so it is bounded first, chunk by chunk, the way the server
|
|
6
|
+
* adapters bound theirs. Each route gets the request size limit its API
|
|
7
|
+
* documents; one environment variable overrides all of them.
|
|
8
|
+
*
|
|
9
|
+
* @module proxy/requestBodyLimit
|
|
10
|
+
*/
|
|
11
|
+
import { RawBodyTooLargeError } from "../server/utils/rawBody.js";
|
|
12
|
+
/**
|
|
13
|
+
* Environment variable that sets the ceiling for every POST route, replacing
|
|
14
|
+
* the per-route defaults below: a byte count, or `64mb` / `512kb` / `1gb`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const PROXY_MAX_REQUEST_BODY_ENV = "NEUROLINK_PROXY_MAX_REQUEST_BODY_BYTES";
|
|
17
|
+
/**
|
|
18
|
+
* 32 MiB. The Claude Messages API and Token Counting API document a 32 MB
|
|
19
|
+
* maximum request size and answer a larger request with 413 `request_too_large`:
|
|
20
|
+
* https://platform.claude.com/docs/en/api/errors#request-size-limits
|
|
21
|
+
*
|
|
22
|
+
* Also the limit for a POST route with no upstream API of its own (the peer
|
|
23
|
+
* endpoints), which has nothing documented to follow.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DEFAULT_PROXY_MAX_REQUEST_BODY_BYTES: number;
|
|
26
|
+
/**
|
|
27
|
+
* 100 MiB. The Gemini API documents inline data as "100 MB per request or
|
|
28
|
+
* payload (50 MB for PDFs)":
|
|
29
|
+
* https://ai.google.dev/gemini-api/docs/file-input-methods
|
|
30
|
+
*/
|
|
31
|
+
export declare const GEMINI_PROXY_MAX_REQUEST_BODY_BYTES: number;
|
|
32
|
+
/**
|
|
33
|
+
* 64 MiB. A local cap, not an OpenAI figure: OpenAI documents no general
|
|
34
|
+
* request-body limit for the Responses or Chat Completions APIs. The only
|
|
35
|
+
* per-request payload figure it states is the image input requirement, "Up to
|
|
36
|
+
* 512 MB total payload per request":
|
|
37
|
+
* https://developers.openai.com/api/docs/guides/images-vision
|
|
38
|
+
* (file inputs are separately limited to 50 MB combined:
|
|
39
|
+
* https://developers.openai.com/api/docs/guides/file-inputs). The proxy holds
|
|
40
|
+
* a body in memory while it reads it, about four times its size, so it does
|
|
41
|
+
* not take the 512 MB figure as its ceiling; a client that sends more raises
|
|
42
|
+
* `NEUROLINK_PROXY_MAX_REQUEST_BODY_BYTES`. The Codex route speaks the
|
|
43
|
+
* Responses format and documents nothing of its own, so it uses the same cap.
|
|
44
|
+
*/
|
|
45
|
+
export declare const OPENAI_PROXY_MAX_REQUEST_BODY_BYTES: number;
|
|
46
|
+
/**
|
|
47
|
+
* The ceiling in bytes for one route: the environment override when it is a
|
|
48
|
+
* positive size, else that route's documented limit.
|
|
49
|
+
*
|
|
50
|
+
* @param routePath - The route as registered, for example `/v1/messages`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveProxyMaxRequestBodyBytes(routePath: string, env?: Record<string, string | undefined>): number;
|
|
53
|
+
/**
|
|
54
|
+
* The request body as text, `undefined` when it cannot be read (a client that
|
|
55
|
+
* hung up mid-body), or the error when it is over the route's ceiling. Over the
|
|
56
|
+
* ceiling the stream is cancelled, so nothing past it is buffered.
|
|
57
|
+
*
|
|
58
|
+
* @param routePath - The route as registered, for example `/v1/messages`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function readProxyRequestText(request: Request, routePath: string, env?: Record<string, string | undefined>): Promise<string | undefined | RawBodyTooLargeError>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-body ceiling for the proxy's POST routes.
|
|
3
|
+
*
|
|
4
|
+
* The body is read before anything else about the request is known (the model,
|
|
5
|
+
* the share token), so it is bounded first, chunk by chunk, the way the server
|
|
6
|
+
* adapters bound theirs. Each route gets the request size limit its API
|
|
7
|
+
* documents; one environment variable overrides all of them.
|
|
8
|
+
*
|
|
9
|
+
* @module proxy/requestBodyLimit
|
|
10
|
+
*/
|
|
11
|
+
import { RawBodyTooLargeError, readBoundedRequestBody, tryParseByteLimit, } from "../server/utils/rawBody.js";
|
|
12
|
+
import { logger } from "../utils/logger.js";
|
|
13
|
+
/**
|
|
14
|
+
* Environment variable that sets the ceiling for every POST route, replacing
|
|
15
|
+
* the per-route defaults below: a byte count, or `64mb` / `512kb` / `1gb`.
|
|
16
|
+
*/
|
|
17
|
+
export const PROXY_MAX_REQUEST_BODY_ENV = "NEUROLINK_PROXY_MAX_REQUEST_BODY_BYTES";
|
|
18
|
+
const MIB = 1024 * 1024;
|
|
19
|
+
/**
|
|
20
|
+
* 32 MiB. The Claude Messages API and Token Counting API document a 32 MB
|
|
21
|
+
* maximum request size and answer a larger request with 413 `request_too_large`:
|
|
22
|
+
* https://platform.claude.com/docs/en/api/errors#request-size-limits
|
|
23
|
+
*
|
|
24
|
+
* Also the limit for a POST route with no upstream API of its own (the peer
|
|
25
|
+
* endpoints), which has nothing documented to follow.
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_PROXY_MAX_REQUEST_BODY_BYTES = 32 * MIB;
|
|
28
|
+
/**
|
|
29
|
+
* 100 MiB. The Gemini API documents inline data as "100 MB per request or
|
|
30
|
+
* payload (50 MB for PDFs)":
|
|
31
|
+
* https://ai.google.dev/gemini-api/docs/file-input-methods
|
|
32
|
+
*/
|
|
33
|
+
export const GEMINI_PROXY_MAX_REQUEST_BODY_BYTES = 100 * MIB;
|
|
34
|
+
/**
|
|
35
|
+
* 64 MiB. A local cap, not an OpenAI figure: OpenAI documents no general
|
|
36
|
+
* request-body limit for the Responses or Chat Completions APIs. The only
|
|
37
|
+
* per-request payload figure it states is the image input requirement, "Up to
|
|
38
|
+
* 512 MB total payload per request":
|
|
39
|
+
* https://developers.openai.com/api/docs/guides/images-vision
|
|
40
|
+
* (file inputs are separately limited to 50 MB combined:
|
|
41
|
+
* https://developers.openai.com/api/docs/guides/file-inputs). The proxy holds
|
|
42
|
+
* a body in memory while it reads it, about four times its size, so it does
|
|
43
|
+
* not take the 512 MB figure as its ceiling; a client that sends more raises
|
|
44
|
+
* `NEUROLINK_PROXY_MAX_REQUEST_BODY_BYTES`. The Codex route speaks the
|
|
45
|
+
* Responses format and documents nothing of its own, so it uses the same cap.
|
|
46
|
+
*/
|
|
47
|
+
export const OPENAI_PROXY_MAX_REQUEST_BODY_BYTES = 64 * MIB;
|
|
48
|
+
/**
|
|
49
|
+
* Registered route path (a suffix, so a base path does not matter) to limit.
|
|
50
|
+
* The first match wins; no match means the default above.
|
|
51
|
+
*/
|
|
52
|
+
const ROUTE_LIMITS = [
|
|
53
|
+
[/\/v1\/messages(?:\/count_tokens)?$/, DEFAULT_PROXY_MAX_REQUEST_BODY_BYTES],
|
|
54
|
+
[/\/v1\/chat\/completions$/, OPENAI_PROXY_MAX_REQUEST_BODY_BYTES],
|
|
55
|
+
[/\/backend-api\/codex\/responses$/, OPENAI_PROXY_MAX_REQUEST_BODY_BYTES],
|
|
56
|
+
[/\/v1beta\/models\/[^/]+$/, GEMINI_PROXY_MAX_REQUEST_BODY_BYTES],
|
|
57
|
+
];
|
|
58
|
+
let warnedAbout;
|
|
59
|
+
/**
|
|
60
|
+
* The ceiling in bytes for one route: the environment override when it is a
|
|
61
|
+
* positive size, else that route's documented limit.
|
|
62
|
+
*
|
|
63
|
+
* @param routePath - The route as registered, for example `/v1/messages`.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveProxyMaxRequestBodyBytes(routePath, env = process.env) {
|
|
66
|
+
const raw = env[PROXY_MAX_REQUEST_BODY_ENV]?.trim();
|
|
67
|
+
if (raw) {
|
|
68
|
+
const parsed = tryParseByteLimit(raw);
|
|
69
|
+
if (parsed !== undefined && parsed > 0) {
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
if (warnedAbout !== raw) {
|
|
73
|
+
warnedAbout = raw;
|
|
74
|
+
logger.always(`[proxy] ignoring ${PROXY_MAX_REQUEST_BODY_ENV}=${JSON.stringify(raw)}: expected a positive byte count or a size such as 64mb; using each route's documented limit`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return (ROUTE_LIMITS.find(([pattern]) => pattern.test(routePath))?.[1] ??
|
|
78
|
+
DEFAULT_PROXY_MAX_REQUEST_BODY_BYTES);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The request body as text, `undefined` when it cannot be read (a client that
|
|
82
|
+
* hung up mid-body), or the error when it is over the route's ceiling. Over the
|
|
83
|
+
* ceiling the stream is cancelled, so nothing past it is buffered.
|
|
84
|
+
*
|
|
85
|
+
* @param routePath - The route as registered, for example `/v1/messages`.
|
|
86
|
+
*/
|
|
87
|
+
export async function readProxyRequestText(request, routePath, env = process.env) {
|
|
88
|
+
try {
|
|
89
|
+
const bytes = await readBoundedRequestBody(request, resolveProxyMaxRequestBodyBytes(routePath, env));
|
|
90
|
+
return bytes instanceof RawBodyTooLargeError
|
|
91
|
+
? bytes
|
|
92
|
+
: new TextDecoder().decode(bytes);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import type { NeuroLink } from "../../neurolink.js";
|
|
7
7
|
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
|
|
8
8
|
import type { MiddlewareDefinition, RouteDefinition, ServerAdapterConfig } from "../../types/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Hono-specific server adapter
|
|
11
|
+
* Supports multiple runtimes: Bun, Deno, Node.js
|
|
12
|
+
*/
|
|
9
13
|
export declare class HonoServerAdapter extends BaseServerAdapter {
|
|
10
14
|
private app;
|
|
11
15
|
private server?;
|
|
@@ -14,6 +18,7 @@ export declare class HonoServerAdapter extends BaseServerAdapter {
|
|
|
14
18
|
private requestContextStore;
|
|
15
19
|
private HTTPExceptionCtor;
|
|
16
20
|
private streamSSEFn;
|
|
21
|
+
private parseFormBody;
|
|
17
22
|
constructor(neurolink: NeuroLink, config?: ServerAdapterConfig);
|
|
18
23
|
/**
|
|
19
24
|
* Create rate limiter middleware for Hono
|
|
@@ -74,5 +79,13 @@ export declare class HonoServerAdapter extends BaseServerAdapter {
|
|
|
74
79
|
getNativeServer(): unknown;
|
|
75
80
|
private extractHeaders;
|
|
76
81
|
private extractQuery;
|
|
82
|
+
/**
|
|
83
|
+
* A request's context, or the 413 that answers a JSON or form body over its
|
|
84
|
+
* limit. That answer is built here, before the context exists, so no
|
|
85
|
+
* middleware (an auth check included) and no route runs for the request.
|
|
86
|
+
* An upload over the limit still reaches its route as the error itself,
|
|
87
|
+
* which answers in its own wire format.
|
|
88
|
+
*/
|
|
89
|
+
private createRequestContext;
|
|
77
90
|
private extractBody;
|
|
78
91
|
}
|
|
@@ -7,31 +7,11 @@ import { logger } from "../../utils/logger.js";
|
|
|
7
7
|
import { AlreadyRunningError, ServerAdapterError, ServerStopError, wrapError, } from "../errors.js";
|
|
8
8
|
import { BaseServerAdapter } from "../abstract/baseServerAdapter.js";
|
|
9
9
|
import { isErrorResponse } from "../utils/validation.js";
|
|
10
|
-
import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError,
|
|
10
|
+
import { isRawBodyContentType, isWebResponse, parseByteLimit, RawBodyTooLargeError, readBoundedRequestBody, } from "../utils/rawBody.js";
|
|
11
11
|
/**
|
|
12
12
|
* Hono-specific server adapter
|
|
13
13
|
* Supports multiple runtimes: Bun, Deno, Node.js
|
|
14
14
|
*/
|
|
15
|
-
/** Raw bodies already read for a request, so a second extraction never hits a consumed stream. */
|
|
16
|
-
const rawBodies = new WeakMap();
|
|
17
|
-
/** A web `ReadableStream` of bytes as an async iterable of Buffers, releasing the reader on exit. */
|
|
18
|
-
async function* webStreamChunks(stream) {
|
|
19
|
-
const reader = stream.getReader();
|
|
20
|
-
try {
|
|
21
|
-
for (;;) {
|
|
22
|
-
const { value, done } = await reader.read();
|
|
23
|
-
if (done) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (value) {
|
|
27
|
-
yield Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
finally {
|
|
32
|
-
reader.releaseLock();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
15
|
export class HonoServerAdapter extends BaseServerAdapter {
|
|
36
16
|
app;
|
|
37
17
|
server;
|
|
@@ -44,6 +24,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
44
24
|
// rely on initializeFramework's own local closure variables.
|
|
45
25
|
HTTPExceptionCtor;
|
|
46
26
|
streamSSEFn;
|
|
27
|
+
parseFormBody;
|
|
47
28
|
constructor(neurolink, config = {}) {
|
|
48
29
|
super(neurolink, config);
|
|
49
30
|
}
|
|
@@ -136,7 +117,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
136
117
|
* adapter (Express/Fastify/Koa) never pulls Hono's import graph in.
|
|
137
118
|
*/
|
|
138
119
|
async initializeFramework() {
|
|
139
|
-
const [{ Hono }, { cors }, { HTTPException }, { logger: honoLogger }, { secureHeaders }, { streamSSE }, { timeout },] = await Promise.all([
|
|
120
|
+
const [{ Hono }, { cors }, { HTTPException }, { logger: honoLogger }, { secureHeaders }, { streamSSE }, { timeout }, { parseBody },] = await Promise.all([
|
|
140
121
|
this.importFrameworkDependency("hono", "hono"),
|
|
141
122
|
this.importFrameworkDependency("hono/cors", "hono"),
|
|
142
123
|
this.importFrameworkDependency("hono/http-exception", "hono"),
|
|
@@ -144,9 +125,11 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
144
125
|
this.importFrameworkDependency("hono/secure-headers", "hono"),
|
|
145
126
|
this.importFrameworkDependency("hono/streaming", "hono"),
|
|
146
127
|
this.importFrameworkDependency("hono/timeout", "hono"),
|
|
128
|
+
this.importFrameworkDependency("hono/utils/body", "hono"),
|
|
147
129
|
]);
|
|
148
130
|
this.HTTPExceptionCtor = HTTPException;
|
|
149
131
|
this.streamSSEFn = streamSSE;
|
|
132
|
+
this.parseFormBody = parseBody;
|
|
150
133
|
this.app = new Hono();
|
|
151
134
|
// Add secure headers
|
|
152
135
|
this.app.use("*", secureHeaders());
|
|
@@ -243,17 +226,12 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
243
226
|
// Extract path parameters
|
|
244
227
|
const params = c.req.param();
|
|
245
228
|
// Reuse existing context from middleware if available, otherwise create new one
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
headers: this.extractHeaders(c),
|
|
253
|
-
query: this.extractQuery(c),
|
|
254
|
-
params,
|
|
255
|
-
body: await this.extractBody(c),
|
|
256
|
-
});
|
|
229
|
+
const created = this.requestContextStore.get(requestId) ||
|
|
230
|
+
(await this.createRequestContext(c, requestId, params));
|
|
231
|
+
if (isWebResponse(created)) {
|
|
232
|
+
return created;
|
|
233
|
+
}
|
|
234
|
+
const ctx = created;
|
|
257
235
|
// Emit request event
|
|
258
236
|
this.emit("request", {
|
|
259
237
|
requestId,
|
|
@@ -484,15 +462,11 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
484
462
|
let ctx = this.requestContextStore.get(requestId);
|
|
485
463
|
if (!ctx) {
|
|
486
464
|
// Create new context for the first middleware
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
query: this.extractQuery(c),
|
|
493
|
-
params,
|
|
494
|
-
body: await this.extractBody(c),
|
|
495
|
-
});
|
|
465
|
+
const created = await this.createRequestContext(c, requestId, params);
|
|
466
|
+
if (isWebResponse(created)) {
|
|
467
|
+
return created;
|
|
468
|
+
}
|
|
469
|
+
ctx = created;
|
|
496
470
|
// Store context for sharing between middleware and route handlers
|
|
497
471
|
this.requestContextStore.set(requestId, ctx);
|
|
498
472
|
}
|
|
@@ -667,22 +641,68 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
667
641
|
});
|
|
668
642
|
return query;
|
|
669
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* A request's context, or the 413 that answers a JSON or form body over its
|
|
646
|
+
* limit. That answer is built here, before the context exists, so no
|
|
647
|
+
* middleware (an auth check included) and no route runs for the request.
|
|
648
|
+
* An upload over the limit still reaches its route as the error itself,
|
|
649
|
+
* which answers in its own wire format.
|
|
650
|
+
*/
|
|
651
|
+
async createRequestContext(c, requestId, params) {
|
|
652
|
+
const body = await this.extractBody(c);
|
|
653
|
+
if (body instanceof RawBodyTooLargeError &&
|
|
654
|
+
!isRawBodyContentType(c.req.header("Content-Type"))) {
|
|
655
|
+
logger.warn("[HonoAdapter] Request rejected", {
|
|
656
|
+
requestId,
|
|
657
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
658
|
+
error: body.message,
|
|
659
|
+
});
|
|
660
|
+
this.emitRequestError({
|
|
661
|
+
requestId,
|
|
662
|
+
error: body,
|
|
663
|
+
timestamp: new Date(),
|
|
664
|
+
});
|
|
665
|
+
return c.json({
|
|
666
|
+
error: { code: "PAYLOAD_TOO_LARGE", message: body.message },
|
|
667
|
+
metadata: { requestId, timestamp: new Date().toISOString() },
|
|
668
|
+
}, 413);
|
|
669
|
+
}
|
|
670
|
+
return this.createContext({
|
|
671
|
+
requestId,
|
|
672
|
+
method: c.req.method,
|
|
673
|
+
path: c.req.path,
|
|
674
|
+
headers: this.extractHeaders(c),
|
|
675
|
+
query: this.extractQuery(c),
|
|
676
|
+
params,
|
|
677
|
+
body,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
670
680
|
async extractBody(c) {
|
|
671
681
|
if (!this.config.bodyParser.enabled) {
|
|
672
682
|
return undefined;
|
|
673
683
|
}
|
|
674
684
|
const contentType = c.req.header("Content-Type") || "";
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
685
|
+
// JSON and url-encoded forms are read through the same bounded read as
|
|
686
|
+
// uploads, with the limits the other adapters apply: `jsonLimit` for JSON,
|
|
687
|
+
// `maxSize` for forms. The bytes are then parsed the way Hono parses a
|
|
688
|
+
// body it has already buffered. A body that cannot be read or parsed is
|
|
689
|
+
// `undefined`, as it was when Hono did the reading.
|
|
690
|
+
const isJson = contentType.includes("application/json");
|
|
691
|
+
if (isJson || contentType.includes("application/x-www-form-urlencoded")) {
|
|
684
692
|
try {
|
|
685
|
-
|
|
693
|
+
const bytes = await readBoundedRequestBody(c.req.raw, parseByteLimit(isJson
|
|
694
|
+
? this.config.bodyParser.jsonLimit
|
|
695
|
+
: this.config.bodyParser.maxSize));
|
|
696
|
+
if (bytes instanceof RawBodyTooLargeError) {
|
|
697
|
+
return bytes;
|
|
698
|
+
}
|
|
699
|
+
return isJson
|
|
700
|
+
? JSON.parse(new TextDecoder().decode(bytes))
|
|
701
|
+
: await this.parseFormBody(new Request(c.req.url, {
|
|
702
|
+
method: "POST",
|
|
703
|
+
headers: { "Content-Type": contentType },
|
|
704
|
+
body: new Uint8Array(bytes),
|
|
705
|
+
}));
|
|
686
706
|
}
|
|
687
707
|
catch {
|
|
688
708
|
return undefined;
|
|
@@ -693,37 +713,7 @@ export class HonoServerAdapter extends BaseServerAdapter {
|
|
|
693
713
|
// a throw from the middleware path would land in onError, which answers
|
|
694
714
|
// 500. The route answers 413 (or a validation error) from it.
|
|
695
715
|
if (isRawBodyContentType(contentType)) {
|
|
696
|
-
|
|
697
|
-
if (Number(c.req.header("Content-Length") ?? 0) > limit) {
|
|
698
|
-
return new RawBodyTooLargeError(limit);
|
|
699
|
-
}
|
|
700
|
-
// Read the web stream chunk by chunk and stop at the limit, so a
|
|
701
|
-
// chunked upload without a Content-Length cannot be buffered whole
|
|
702
|
-
// (and that before any route logic, including the API-key check, runs).
|
|
703
|
-
// The body is extracted more than once per request (middleware and
|
|
704
|
-
// route); a web stream can only be read once, so the outcome is kept
|
|
705
|
-
// per request the way Hono caches `arrayBuffer()`.
|
|
706
|
-
const cached = rawBodies.get(c.req.raw);
|
|
707
|
-
if (cached !== undefined) {
|
|
708
|
-
return cached;
|
|
709
|
-
}
|
|
710
|
-
const stream = c.req.raw.body;
|
|
711
|
-
if (!stream) {
|
|
712
|
-
return Buffer.alloc(0);
|
|
713
|
-
}
|
|
714
|
-
let outcome;
|
|
715
|
-
try {
|
|
716
|
-
outcome = await readRawBody(webStreamChunks(stream), limit);
|
|
717
|
-
}
|
|
718
|
-
catch (error) {
|
|
719
|
-
if (!(error instanceof RawBodyTooLargeError)) {
|
|
720
|
-
throw error;
|
|
721
|
-
}
|
|
722
|
-
await stream.cancel().catch(() => undefined);
|
|
723
|
-
outcome = error;
|
|
724
|
-
}
|
|
725
|
-
rawBodies.set(c.req.raw, outcome);
|
|
726
|
-
return outcome;
|
|
716
|
+
return readBoundedRequestBody(c.req.raw, parseByteLimit(this.config.bodyParser.rawMaxSize));
|
|
727
717
|
}
|
|
728
718
|
return undefined;
|
|
729
719
|
}
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
/** `true` for a content type whose body the adapters pass through unparsed. */
|
|
15
15
|
export declare function isRawBodyContentType(contentType: unknown): boolean;
|
|
16
|
+
/** "10mb" / "512kb" / "1gb" / a byte count → bytes, or `undefined` when it is none of those. */
|
|
17
|
+
export declare function tryParseByteLimit(limit: string | number | undefined): number | undefined;
|
|
16
18
|
/** "10mb" / "512kb" / "1gb" / a byte count → bytes. Falls back to 10 MB. */
|
|
17
19
|
export declare function parseByteLimit(limit: string | number | undefined): number;
|
|
18
20
|
/** Thrown when a raw body grows past the adapter's `maxSize`. */
|
|
@@ -22,6 +24,21 @@ export declare class RawBodyTooLargeError extends Error {
|
|
|
22
24
|
}
|
|
23
25
|
/** Read a Node request stream into one `Buffer`, refusing more than `maxBytes`. */
|
|
24
26
|
export declare function readRawBody(stream: AsyncIterable<Buffer | string>, maxBytes: number): Promise<Buffer>;
|
|
27
|
+
/** A web `ReadableStream` of bytes as an async iterable of Buffers, releasing the reader on exit. */
|
|
28
|
+
export declare function webStreamChunks(stream: ReadableStream<Uint8Array>): AsyncIterable<Buffer>;
|
|
29
|
+
/**
|
|
30
|
+
* Read a web `Request` body, refusing more than `maxBytes`.
|
|
31
|
+
*
|
|
32
|
+
* A declared `Content-Length` over the limit is refused without reading
|
|
33
|
+
* anything. Otherwise the stream is read chunk by chunk and stopped at the
|
|
34
|
+
* limit, so a chunked body without a `Content-Length` cannot be buffered whole.
|
|
35
|
+
* Over the limit the result is the error itself rather than a throw: a throw
|
|
36
|
+
* from a middleware lands in the framework's error handler, which answers 500.
|
|
37
|
+
*
|
|
38
|
+
* The outcome is kept per request, because a web stream can only be read once
|
|
39
|
+
* and the body may be asked for more than once (middleware, then route).
|
|
40
|
+
*/
|
|
41
|
+
export declare function readBoundedRequestBody(request: Request, maxBytes: number): Promise<Buffer | RawBodyTooLargeError>;
|
|
25
42
|
/** A web `Response` returned by a route handler. */
|
|
26
43
|
export declare function isWebResponse(value: unknown): value is Response;
|
|
27
44
|
/** Status, headers and body bytes of a web `Response`, for adapters that write to a Node response. */
|
|
@@ -17,8 +17,8 @@ const RAW_BODY_CONTENT_TYPE = /^\s*(multipart\/form-data|application\/octet-stre
|
|
|
17
17
|
export function isRawBodyContentType(contentType) {
|
|
18
18
|
return (typeof contentType === "string" && RAW_BODY_CONTENT_TYPE.test(contentType));
|
|
19
19
|
}
|
|
20
|
-
/** "10mb" / "512kb" / "1gb" / a byte count → bytes
|
|
21
|
-
export function
|
|
20
|
+
/** "10mb" / "512kb" / "1gb" / a byte count → bytes, or `undefined` when it is none of those. */
|
|
21
|
+
export function tryParseByteLimit(limit) {
|
|
22
22
|
if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) {
|
|
23
23
|
return limit;
|
|
24
24
|
}
|
|
@@ -26,7 +26,7 @@ export function parseByteLimit(limit) {
|
|
|
26
26
|
? limit.trim().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/i)
|
|
27
27
|
: null;
|
|
28
28
|
if (!match) {
|
|
29
|
-
return
|
|
29
|
+
return undefined;
|
|
30
30
|
}
|
|
31
31
|
const units = {
|
|
32
32
|
b: 1,
|
|
@@ -34,7 +34,14 @@ export function parseByteLimit(limit) {
|
|
|
34
34
|
mb: 1024 * 1024,
|
|
35
35
|
gb: 1024 * 1024 * 1024,
|
|
36
36
|
};
|
|
37
|
-
|
|
37
|
+
const bytes = Math.floor(Number(match[1]) * units[(match[2] ?? "b").toLowerCase()]);
|
|
38
|
+
// A digit string long enough to overflow reads as Infinity, which no body
|
|
39
|
+
// can exceed: treat it as unparseable so the caller's default applies.
|
|
40
|
+
return Number.isFinite(bytes) ? bytes : undefined;
|
|
41
|
+
}
|
|
42
|
+
/** "10mb" / "512kb" / "1gb" / a byte count → bytes. Falls back to 10 MB. */
|
|
43
|
+
export function parseByteLimit(limit) {
|
|
44
|
+
return tryParseByteLimit(limit) ?? 10 * 1024 * 1024;
|
|
38
45
|
}
|
|
39
46
|
/** Thrown when a raw body grows past the adapter's `maxSize`. */
|
|
40
47
|
export class RawBodyTooLargeError extends Error {
|
|
@@ -58,6 +65,64 @@ export async function readRawBody(stream, maxBytes) {
|
|
|
58
65
|
}
|
|
59
66
|
return Buffer.concat(chunks);
|
|
60
67
|
}
|
|
68
|
+
/** A web `ReadableStream` of bytes as an async iterable of Buffers, releasing the reader on exit. */
|
|
69
|
+
export async function* webStreamChunks(stream) {
|
|
70
|
+
const reader = stream.getReader();
|
|
71
|
+
try {
|
|
72
|
+
for (;;) {
|
|
73
|
+
const { value, done } = await reader.read();
|
|
74
|
+
if (done) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (value) {
|
|
78
|
+
yield Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
reader.releaseLock();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Bodies already read for a request, so a second read never hits a consumed stream. */
|
|
87
|
+
const boundedBodies = new WeakMap();
|
|
88
|
+
/**
|
|
89
|
+
* Read a web `Request` body, refusing more than `maxBytes`.
|
|
90
|
+
*
|
|
91
|
+
* A declared `Content-Length` over the limit is refused without reading
|
|
92
|
+
* anything. Otherwise the stream is read chunk by chunk and stopped at the
|
|
93
|
+
* limit, so a chunked body without a `Content-Length` cannot be buffered whole.
|
|
94
|
+
* Over the limit the result is the error itself rather than a throw: a throw
|
|
95
|
+
* from a middleware lands in the framework's error handler, which answers 500.
|
|
96
|
+
*
|
|
97
|
+
* The outcome is kept per request, because a web stream can only be read once
|
|
98
|
+
* and the body may be asked for more than once (middleware, then route).
|
|
99
|
+
*/
|
|
100
|
+
export async function readBoundedRequestBody(request, maxBytes) {
|
|
101
|
+
if (Number(request.headers.get("content-length") ?? 0) > maxBytes) {
|
|
102
|
+
return new RawBodyTooLargeError(maxBytes);
|
|
103
|
+
}
|
|
104
|
+
const cached = boundedBodies.get(request);
|
|
105
|
+
if (cached !== undefined) {
|
|
106
|
+
return cached;
|
|
107
|
+
}
|
|
108
|
+
const stream = request.body;
|
|
109
|
+
if (!stream) {
|
|
110
|
+
return Buffer.alloc(0);
|
|
111
|
+
}
|
|
112
|
+
let outcome;
|
|
113
|
+
try {
|
|
114
|
+
outcome = await readRawBody(webStreamChunks(stream), maxBytes);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (!(error instanceof RawBodyTooLargeError)) {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
await stream.cancel().catch(() => undefined);
|
|
121
|
+
outcome = error;
|
|
122
|
+
}
|
|
123
|
+
boundedBodies.set(request, outcome);
|
|
124
|
+
return outcome;
|
|
125
|
+
}
|
|
61
126
|
/** A web `Response` returned by a route handler. */
|
|
62
127
|
export function isWebResponse(value) {
|
|
63
128
|
return typeof Response !== "undefined" && value instanceof Response;
|