Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions docs/2.utils/1.request.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ icon: material-symbols-light:input

<!-- automd:jsdocs src="../../src/utils/body.ts" -->

### `assertBodySize(event, limit)`

Asserts that request body size is within the specified limit.

If body size exceeds the limit, throws a `413` Request Entity Too Large response error.

**Example:**

```ts
app.get("/", async (event) => {
await assertBodySize(event, 10 * 1024 * 1024); // 10MB
const data = await event.req.formData();
});
```

### `readBody(event)`

Reads request body and tries to parse using JSON.parse or URLSearchParams.
Expand All @@ -31,22 +46,23 @@ You can use a simple function to validate the body or use a Standard-Schema comp
**Example:**

```ts
function validateBody(body: any) {
return typeof body === "object" && body !== null;
};
app.get("/", async (event) => {
const body = await readValidatedBody(event, (body) => {
return typeof body === "object" && body !== null;
});
const body = await readValidatedBody(event, validateBody);
});
```

**Example:**

```ts
import { z } from "zod";
const objectSchema = z.object({
name: z.string().min(3).max(20),
age: z.number({ coerce: true }).positive().int(),
});
app.get("/", async (event) => {
const objectSchema = z.object({
name: z.string().min(3).max(20),
age: z.number({ coerce: true }).positive().int(),
});
const body = await readValidatedBody(event, objectSchema);
});
```
Expand Down
6 changes: 6 additions & 0 deletions docs/2.utils/9.more.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ Checks if the input is an object with `{ req: Request }` signature.

<!-- automd:jsdocs src="../../src/utils/middleware.ts" -->

### `bodyLimit(limit)`

Define a middleware that checks whether request body size is within specified limit.

If body size exceeds the limit, throws a `413` Request Entity Too Large response error. If you need custom handling for this case, use `assertBodySize` instead.

### `onError(hook)`

Define a middleware that runs when an error occurs.
Expand Down
7 changes: 5 additions & 2 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
packages:
- "playground"
- "examples"
- playground
- examples

ignoredBuiltDependencies:
- esbuild
9 changes: 7 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,12 @@ export {

// Middleware

export { onError, onRequest, onResponse } from "./utils/middleware.ts";
export {
onError,
onRequest,
onResponse,
bodyLimit,
} from "./utils/middleware.ts";

// Proxy

Expand All @@ -129,7 +134,7 @@ export {

// Body

export { readBody, readValidatedBody } from "./utils/body.ts";
export { readBody, readValidatedBody, assertBodySize } from "./utils/body.ts";

// Cookie

Expand Down
74 changes: 67 additions & 7 deletions src/utils/body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,22 @@ export async function readValidatedBody<
* You can use a simple function to validate the body or use a Standard-Schema compatible library like `zod` to define a schema.
*
* @example
* function validateBody(body: any) {
* return typeof body === "object" && body !== null;
* };
*
* app.get("/", async (event) => {
* const body = await readValidatedBody(event, (body) => {
* return typeof body === "object" && body !== null;
* });
* const body = await readValidatedBody(event, validateBody);
* });
* @example
* import { z } from "zod";
*
* const objectSchema = z.object({
* name: z.string().min(3).max(20),
* age: z.number({ coerce: true }).positive().int(),
* });
*
* app.get("/", async (event) => {
* const objectSchema = z.object({
* name: z.string().min(3).max(20),
* age: z.number({ coerce: true }).positive().int(),
* });
* const body = await readValidatedBody(event, objectSchema);
* });
*
Expand All @@ -98,3 +101,60 @@ export async function readValidatedBody(
const _body = await readBody(event);
return validateData(_body, validate);
}

/**
* Asserts that request body size is within the specified limit.
*
* If body size exceeds the limit, throws a `413` Request Entity Too Large response error.
*
* @example
* app.get("/", async (event) => {
* await assertBodySize(event, 10 * 1024 * 1024); // 10MB
* const data = await event.req.formData();
* });
*
* @param event HTTP event
* @param limit Body size limit in bytes
*/
export async function assertBodySize(
event: HTTPEvent,
limit: number,
): Promise<void> {
const isWithin = await isBodySizeWithin(event, limit);
if (!isWithin) {
throw new HTTPError({
status: 413,
statusText: "Request Entity Too Large",
message: `Request body size exceeds the limit of ${limit} bytes`,
});
}
}

// Internal util for now. We can export later if needed
async function isBodySizeWithin(
event: HTTPEvent,
limit: number,
): Promise<boolean> {
const req = event.req;
if (req.body === null) {
return true;
}

const bodyLen = req.headers.get("content-length");
if (bodyLen !== null && !req.headers.has("transfer-encoding")) {
return +bodyLen <= limit;
}
Comment on lines +143 to +146

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pi0 see #1150 (comment)

by your previous statement this code is insecure

Copy link
Member

@pi0 pi0 Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a flag to force check when no length.

But also test which runtimes allow body with length (invalid) + transfer-encoding (if they do yes indeed we should enable flag by default)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the HTTP/1.1 specification (RFC 7230, section 3.3.2):

A message must not include both a Content-Length header field and a Transfer-Encoding header field.

(we should actually fail in this case)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it stills allows a faked content-length

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running in real HTTP server (Node.js server for example) it should stop reading body when content-length excceds as it violated protocol.

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Bun and Deno also "cut off" reading body by validating content-length)

image

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much


const reader = req.clone().body!.getReader();
let chunk = await reader.read();
let size = 0;
while (!chunk.done) {
size += chunk.value.byteLength;
if (size > limit) {
return false;
}
chunk = await reader.read();
}

return true;
}
17 changes: 17 additions & 0 deletions src/utils/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { MaybePromise } from "../types/_utils.ts";

import type { H3Event } from "../event.ts";
import type { Middleware } from "../types/handler.ts";
import { assertBodySize } from "./body.ts";

/**
* Define a middleware that runs on each request.
Expand Down Expand Up @@ -63,3 +64,19 @@ export function onError(
}
};
}

/**
* Define a middleware that checks whether request body size is within specified limit.
*
* If body size exceeds the limit, throws a `413` Request Entity Too Large response error.
* If you need custom handling for this case, use `assertBodySize` instead.
*
* @param limit Body size limit in bytes
* @see {assertBodySize}
*/
export function bodyLimit(limit: number): Middleware {
return async (event, next) => {
await assertBodySize(event, limit);
return next();
};
}
80 changes: 80 additions & 0 deletions test/unit/body-limit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { expect, it, describe } from "vitest";
import { mockEvent, assertBodySize, HTTPError } from "../../src/index.ts";

describe("body limit (unit)", () => {
const streamBytesFrom = (it: Iterable<any, any>) =>
new ReadableStream({
start(c) {
for (const part of it) c.enqueue(part);
c.close();
},
}).pipeThrough(new TextEncoderStream());

describe("assertBodySize", () => {
it("buffered body", async () => {
const BODY = "a small request body";

const eventMock = mockEvent("/", {
method: "POST",
body: BODY,
});

await expect(
assertBodySize(eventMock, BODY.length),
).resolves.toBeUndefined();
await expect(
assertBodySize(eventMock, BODY.length + 10),
).resolves.toBeUndefined();
await expect(assertBodySize(eventMock, BODY.length - 2)).rejects.toThrow(
HTTPError,
);
});

it("streaming body", async () => {
const BODY_PARTS = [
"parts",
"of",
"the",
"body",
"that",
"are",
"streamed",
"in",
];

const eventMock = mockEvent("/", {
method: "POST",
body: streamBytesFrom(BODY_PARTS),
});

await expect(assertBodySize(eventMock, 100)).resolves.toBeUndefined();
await expect(assertBodySize(eventMock, 10)).rejects.toThrow(HTTPError);
});

it("streaming body with content-length header", async () => {
const BODY_PARTS = [
"parts",
"of",
"the",
"body",
"that",
"are",
"streamed",
"in",
];

const eventMock = mockEvent("/", {
method: "POST",
body: streamBytesFrom(BODY_PARTS),
headers: {
// Should ignore content-length
"content-length": "7",
"transfer-encoding": "chunked",
},
});

await expect(assertBodySize(eventMock, 100)).resolves.toBeUndefined();
await expect(assertBodySize(eventMock, 10)).rejects.toThrow(HTTPError);
});
});
});
2 changes: 2 additions & 0 deletions test/unit/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ describe("h3 package", () => {
"appendHeaders",
"appendResponseHeader",
"appendResponseHeaders",
"assertBodySize",
"assertMethod",
"basicAuth",
"bodyLimit",
"callMiddleware",
"clearResponseHeaders",
"clearSession",
Expand Down
Loading