Skip to content

Commit dfad201

Browse files
Theodor N. EngøyTheodor N. Engøy
authored andcommitted
hono: add JSON request body size limit
1 parent 65bbcea commit dfad201

3 files changed

Lines changed: 82 additions & 3 deletions

File tree

packages/middleware/hono/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ const app = createMcpHonoApp();
3838
app.all('/mcp', c => transport.handleRequest(c.req.raw, { parsedBody: c.get('parsedBody') }));
3939
```
4040

41+
### Options
42+
43+
`createMcpHonoApp({ ... })` supports:
44+
45+
- `host` (default: `127.0.0.1`): used for localhost DNS rebinding protection behavior
46+
- `allowedHosts` (optional): explicit allowed hostnames
47+
- `maxBodyBytes` (default: `1_000_000`): maximum JSON request body size parsed by the built-in middleware
48+
4149
### Host header validation (DNS rebinding protection)
4250

4351
```ts

packages/middleware/hono/src/hono.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,39 @@ import { Hono } from 'hono';
33

44
import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js';
55

6+
const DEFAULT_MAX_BODY_BYTES = 1_000_000; // 1MB
7+
8+
async function readRequestTextWithLimit(req: Request, maxBytes: number): Promise<string> {
9+
const body = req.body;
10+
if (!body) return '';
11+
12+
const reader = body.getReader();
13+
const chunks: Uint8Array[] = [];
14+
let total = 0;
15+
16+
while (true) {
17+
const { value, done } = await reader.read();
18+
if (done) break;
19+
if (!value) continue;
20+
21+
total += value.byteLength;
22+
if (total > maxBytes) {
23+
void reader.cancel().catch(() => {});
24+
throw new Error('payload_too_large');
25+
}
26+
chunks.push(value);
27+
}
28+
29+
const out = new Uint8Array(total);
30+
let offset = 0;
31+
for (const c of chunks) {
32+
out.set(c, offset);
33+
offset += c.byteLength;
34+
}
35+
36+
return new TextDecoder().decode(out);
37+
}
38+
639
/**
740
* Options for creating an MCP Hono application.
841
*/
@@ -22,6 +55,14 @@ export interface CreateMcpHonoAppOptions {
2255
* to restrict which hostnames are allowed.
2356
*/
2457
allowedHosts?: string[];
58+
59+
/**
60+
* Maximum JSON request body size in bytes.
61+
* Used by the built-in JSON parsing middleware for basic DoS resistance.
62+
*
63+
* @default 1_000_000 (1 MB)
64+
*/
65+
maxBodyBytes?: number;
2566
}
2667

2768
/**
@@ -39,7 +80,7 @@ export interface CreateMcpHonoAppOptions {
3980
* @returns A configured Hono application
4081
*/
4182
export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono {
42-
const { host = '127.0.0.1', allowedHosts } = options;
83+
const { host = '127.0.0.1', allowedHosts, maxBodyBytes = DEFAULT_MAX_BODY_BYTES } = options;
4384

4485
const app = new Hono();
4586

@@ -55,9 +96,26 @@ export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono {
5596
return await next();
5697
}
5798

99+
// Fast-path: reject known oversized payloads without reading.
100+
const clRaw = c.req.header('content-length') ?? '';
101+
const cl = Number(clRaw);
102+
if (Number.isFinite(cl) && cl > maxBodyBytes) {
103+
return c.text('Payload too large', 413);
104+
}
105+
106+
// Parse from a clone so we don't consume the original request stream.
107+
let text: string;
108+
try {
109+
text = await readRequestTextWithLimit(c.req.raw.clone(), maxBodyBytes);
110+
} catch (error) {
111+
if (error instanceof Error && error.message === 'payload_too_large') {
112+
return c.text('Payload too large', 413);
113+
}
114+
return c.text('Invalid JSON', 400);
115+
}
116+
58117
try {
59-
// Parse from a clone so we don't consume the original request stream.
60-
const parsed = await c.req.raw.clone().json();
118+
const parsed = JSON.parse(text);
61119
c.set('parsedBody', parsed);
62120
} catch {
63121
// Mirror express.json() behavior loosely: reject invalid JSON.

packages/middleware/hono/test/hono.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,19 @@ describe('@modelcontextprotocol/hono', () => {
7777
expect(await res.json()).toEqual({ a: 1 });
7878
});
7979

80+
test('createMcpHonoApp returns 413 on oversized JSON bodies', async () => {
81+
const app = createMcpHonoApp({ maxBodyBytes: 10 });
82+
app.post('/echo', (c: Context) => c.text('ok'));
83+
84+
const res = await app.request('http://localhost/echo', {
85+
method: 'POST',
86+
headers: { Host: 'localhost:3000', 'content-type': 'application/json' },
87+
body: JSON.stringify({ a: '0123456789' })
88+
});
89+
expect(res.status).toBe(413);
90+
expect(await res.text()).toBe('Payload too large');
91+
});
92+
8093
test('createMcpHonoApp returns 400 on invalid JSON', async () => {
8194
const app = createMcpHonoApp();
8295
app.post('/echo', (c: Context) => c.text('ok'));

0 commit comments

Comments
 (0)