-
Couldn't load subscription status.
- Fork 295
feat: request body limit #1150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: request body limit #1150
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import type { H3Event } from "../event.ts"; | ||
| import type { H3Plugin } from "../types/h3.ts"; | ||
| import { HTTPError } from "../error.ts"; | ||
| import { defineMiddleware } from "../middleware.ts"; | ||
|
|
||
| export interface BodySizeLimitOptions { | ||
| /** | ||
| * Maximum allowed body size in bytes | ||
| */ | ||
| maxSize: number; | ||
| /** | ||
| * Routes to apply the limit to (optional) | ||
| * If not specified, applies to all routes | ||
| */ | ||
| routes?: Array<string | RegExp>; | ||
| /** | ||
| * Routes to exclude from the limit (optional) | ||
| */ | ||
| exclude?: Array<string | RegExp>; | ||
| } | ||
|
|
||
| /** | ||
| * Define a plugin that limits request body size | ||
| * | ||
| * @example | ||
| * ```js | ||
| * import { defineBodySizeLimitPlugin } from "h3"; | ||
| * | ||
| * const bodySizeLimit = defineBodySizeLimitPlugin({ | ||
| * maxSize: 1024 * 1024, // 1MB | ||
| * routes: ["/api/upload", /^\/api\/files/], | ||
| * exclude: ["/api/large-upload"] | ||
| * }); | ||
| * | ||
| * app.register(bodySizeLimit); | ||
| * ``` | ||
| */ | ||
| export function defineBodySizeLimitPlugin( | ||
| options: BodySizeLimitOptions, | ||
| ): H3Plugin { | ||
| return (h3) => { | ||
| h3.use( | ||
| defineMiddleware(async (event: H3Event) => { | ||
| const url = event.req.url; | ||
| const path = url ? new URL(url, "http://localhost").pathname : "/"; | ||
pi0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Check if route should be excluded | ||
| if (options.exclude) { | ||
| for (const pattern of options.exclude) { | ||
| if (typeof pattern === "string" && path === pattern) return; | ||
| if (pattern instanceof RegExp && pattern.test(path)) return; | ||
| } | ||
| } | ||
pi0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Check if route matches (if routes specified) | ||
| if (options.routes) { | ||
| let matches = false; | ||
| for (const pattern of options.routes) { | ||
| if (typeof pattern === "string" && path === pattern) { | ||
| matches = true; | ||
| break; | ||
| } | ||
| if (pattern instanceof RegExp && pattern.test(path)) { | ||
| matches = true; | ||
| break; | ||
| } | ||
| } | ||
| if (!matches) return; | ||
| } | ||
|
|
||
| // Check Content-Length header first | ||
| const contentLength = event.req.headers.get("content-length"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not a safe implementation as @OskarLebuda pointed out. We need to both check header and transform body stream into another controller that force-stops reading body as soon as max length is reached. |
||
| if (contentLength) { | ||
| const size = Number.parseInt(contentLength, 10); | ||
| if (!Number.isNaN(size) && size > options.maxSize) { | ||
| throw new HTTPError({ | ||
| status: 413, | ||
| statusText: "Payload Too Large", | ||
| message: `Request body size ${size} exceeds the limit of ${options.maxSize} bytes`, | ||
| }); | ||
| } | ||
| } | ||
| }), | ||
pi0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ); | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { defineBodySizeLimitPlugin, defineRoute } from "../src/index.ts"; | ||
| import { H3 } from "../src/h3.ts"; | ||
|
|
||
| describe("defineBodySizeLimitPlugin", () => { | ||
| it("should limit body size for all routes", async () => { | ||
| const app = new H3(); | ||
|
|
||
| // Register body size limit plugin | ||
| const bodySizeLimit = defineBodySizeLimitPlugin({ | ||
| maxSize: 1024, // 1KB | ||
| }); | ||
| app.register(bodySizeLimit); | ||
|
|
||
| // Register a route | ||
| app.on("POST", "/upload", () => "ok"); | ||
|
|
||
| // Test with small body (should succeed) | ||
| const smallBody = "a".repeat(500); | ||
| const smallRes = await app.fetch("/upload", { | ||
| method: "POST", | ||
| body: smallBody, | ||
| headers: { | ||
| "Content-Type": "text/plain", | ||
| "Content-Length": String(smallBody.length), | ||
| }, | ||
| }); | ||
| expect(await smallRes.text()).toBe("ok"); | ||
|
|
||
| // Test with large body (should fail) | ||
| const largeBody = "a".repeat(2000); | ||
| const largeRes = await app.fetch("/upload", { | ||
| method: "POST", | ||
| body: largeBody, | ||
| headers: { | ||
| "Content-Type": "text/plain", | ||
| "Content-Length": String(largeBody.length), | ||
| }, | ||
| }); | ||
| expect(largeRes.status).toBe(413); | ||
| const error = await largeRes.json(); | ||
| expect(error.statusText).toBe("Payload Too Large"); | ||
| }); | ||
|
|
||
| it("should limit body size only for specified routes", async () => { | ||
| const app = new H3(); | ||
|
|
||
| // Register body size limit plugin for specific routes | ||
| const bodySizeLimit = defineBodySizeLimitPlugin({ | ||
| maxSize: 1024, // 1KB | ||
| routes: ["/api/upload", /^\/api\/files/], | ||
| }); | ||
| app.register(bodySizeLimit); | ||
|
|
||
| // Register routes | ||
| app.on("POST", "/api/upload", () => "upload ok"); | ||
| app.on("POST", "/api/files/test", () => "files ok"); | ||
| app.on("POST", "/other", () => "other ok"); | ||
|
|
||
| const largeBody = "a".repeat(2000); | ||
|
|
||
| // Should fail for /api/upload | ||
| const uploadRes = await app.fetch("/api/upload", { | ||
| method: "POST", | ||
| body: largeBody, | ||
| headers: { | ||
| "Content-Type": "text/plain", | ||
| "Content-Length": String(largeBody.length), | ||
| }, | ||
| }); | ||
| expect(uploadRes.status).toBe(413); | ||
|
|
||
| // Should fail for /api/files/test | ||
| const filesRes = await app.fetch("/api/files/test", { | ||
| method: "POST", | ||
| body: largeBody, | ||
| headers: { | ||
| "Content-Type": "text/plain", | ||
| "Content-Length": String(largeBody.length), | ||
| }, | ||
| }); | ||
| expect(filesRes.status).toBe(413); | ||
|
|
||
| // Should succeed for /other | ||
| const otherRes = await app.fetch("/other", { | ||
| method: "POST", | ||
| body: largeBody, | ||
| headers: { | ||
| "Content-Type": "text/plain", | ||
| }, | ||
| }); | ||
| expect(await otherRes.text()).toBe("other ok"); | ||
| }); | ||
|
|
||
| it("should exclude specified routes from limit", async () => { | ||
| const app = new H3(); | ||
|
|
||
| // Register body size limit plugin with exclusions | ||
| const bodySizeLimit = defineBodySizeLimitPlugin({ | ||
| maxSize: 1024, // 1KB | ||
| exclude: ["/api/large-upload"], | ||
| }); | ||
| app.register(bodySizeLimit); | ||
|
|
||
| // Register routes | ||
| app.on("POST", "/api/upload", () => "upload ok"); | ||
| app.on("POST", "/api/large-upload", () => "large upload ok"); | ||
|
|
||
| const largeBody = "a".repeat(2000); | ||
|
|
||
| // Should fail for /api/upload | ||
| const uploadRes = await app.fetch("/api/upload", { | ||
| method: "POST", | ||
| body: largeBody, | ||
| headers: { | ||
| "Content-Type": "text/plain", | ||
| "Content-Length": String(largeBody.length), | ||
| }, | ||
| }); | ||
| expect(uploadRes.status).toBe(413); | ||
|
|
||
| // Should succeed for /api/large-upload (excluded) | ||
| const largeUploadRes = await app.fetch("/api/large-upload", { | ||
| method: "POST", | ||
| body: largeBody, | ||
| headers: { | ||
| "Content-Type": "text/plain", | ||
| }, | ||
| }); | ||
| expect(await largeUploadRes.text()).toBe("large upload ok"); | ||
| }); | ||
|
|
||
| it("should work with defineRoute", async () => { | ||
| const app = new H3(); | ||
|
|
||
| // Register body size limit plugin | ||
| const bodySizeLimit = defineBodySizeLimitPlugin({ | ||
| maxSize: 1024, // 1KB | ||
| }); | ||
| app.register(bodySizeLimit); | ||
|
|
||
| // Register route using defineRoute | ||
| const uploadRoute = defineRoute({ | ||
| method: "POST", | ||
| route: "/upload", | ||
| handler: () => "ok", | ||
| }); | ||
| app.register(uploadRoute); | ||
|
|
||
| // Test with large body (should fail) | ||
| const largeBody = "a".repeat(2000); | ||
| const largeRes = await app.fetch("/upload", { | ||
| method: "POST", | ||
| body: largeBody, | ||
| headers: { | ||
| "Content-Type": "text/plain", | ||
| "Content-Length": String(largeBody.length), | ||
| }, | ||
| }); | ||
| expect(largeRes.status).toBe(413); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be a
middlewarenot a plugin to apply in places needed.With middleware, pattern and route matching can be defined also on definition therefore we do not need to (re)implement logic inside this utility.