Skip to content

Commit 1b276c9

Browse files
ztannerijjk
andcommitted
[backport]: experimental.middlewareClientMaxBodySize (#84722)
Backports: - #84539 - #84712 --------- Co-authored-by: JJ Kasper <[email protected]>
1 parent 2061f04 commit 1b276c9

File tree

12 files changed

+483
-6
lines changed

12 files changed

+483
-6
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
title: experimental.middlewareClientMaxBodySize
3+
description: Configure the maximum request body size when using middleware.
4+
version: experimental
5+
---
6+
7+
When middleware is used, Next.js automatically clones the request body and buffers it in memory to enable multiple reads - both in middleware and the underlying route handler. To prevent excessive memory usage, this configuration option sets a size limit on the buffered body.
8+
9+
By default, the maximum body size is **10MB**. If a request body exceeds this limit, the body will only be buffered up to the limit, and a warning will be logged indicating which route exceeded the limit.
10+
11+
## Options
12+
13+
### String format (recommended)
14+
15+
Specify the size using a human-readable string format:
16+
17+
```ts filename="next.config.ts" switcher
18+
import type { NextConfig } from 'next'
19+
20+
const nextConfig: NextConfig = {
21+
experimental: {
22+
middlewareClientMaxBodySize: '1mb',
23+
},
24+
}
25+
26+
export default nextConfig
27+
```
28+
29+
```js filename="next.config.js" switcher
30+
/** @type {import('next').NextConfig} */
31+
const nextConfig = {
32+
experimental: {
33+
middlewareClientMaxBodySize: '1mb',
34+
},
35+
}
36+
37+
module.exports = nextConfig
38+
```
39+
40+
Supported units: `b`, `kb`, `mb`, `gb`
41+
42+
### Number format
43+
44+
Alternatively, specify the size in bytes as a number:
45+
46+
```ts filename="next.config.ts" switcher
47+
import type { NextConfig } from 'next'
48+
49+
const nextConfig: NextConfig = {
50+
experimental: {
51+
middlewareClientMaxBodySize: 1048576, // 1MB in bytes
52+
},
53+
}
54+
55+
export default nextConfig
56+
```
57+
58+
```js filename="next.config.js" switcher
59+
/** @type {import('next').NextConfig} */
60+
const nextConfig = {
61+
experimental: {
62+
middlewareClientMaxBodySize: 1048576, // 1MB in bytes
63+
},
64+
}
65+
66+
module.exports = nextConfig
67+
```
68+
69+
## Behavior
70+
71+
When a request body exceeds the configured limit:
72+
73+
1. Next.js will buffer only the first N bytes (up to the limit)
74+
2. A warning will be logged to the console indicating the route that exceeded the limit
75+
3. The request will continue processing normally, but only the partial body will be available
76+
4. The request will **not** fail or return an error to the client
77+
78+
If your application needs to process the full request body, you should either:
79+
80+
- Increase the `middlewareClientMaxBodySize` limit
81+
- Handle the partial body gracefully in your application logic
82+
83+
## Example
84+
85+
```ts filename="middleware.ts"
86+
import { NextRequest, NextResponse } from 'next/server'
87+
88+
export async function middleware(request: NextRequest) {
89+
// Next.js automatically buffers the body with the configured size limit
90+
// You can read the body in middleware...
91+
const body = await request.text()
92+
93+
// If the body exceeded the limit, only partial data will be available
94+
console.log('Body size:', body.length)
95+
96+
return NextResponse.next()
97+
}
98+
```
99+
100+
```ts filename="app/api/upload/route.ts"
101+
import { NextRequest, NextResponse } from 'next/server'
102+
103+
export async function POST(request: NextRequest) {
104+
// ...and the body is still available in your route handler
105+
const body = await request.text()
106+
107+
console.log('Body in route handler:', body.length)
108+
109+
return NextResponse.json({ received: body.length })
110+
}
111+
```
112+
113+
## Good to know
114+
115+
- This setting only applies when middleware is used in your application
116+
- The default limit of 10MB is designed to balance memory usage and typical use cases
117+
- The limit applies per-request, not globally across all concurrent requests
118+
- For applications handling large file uploads, consider increasing the limit accordingly
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: experimental.middlewareClientMaxBodySize
3+
description: Configure the maximum request body size when using middleware.
4+
source: app/api-reference/config/next-config-js/middlewareClientMaxBodySize
5+
---
6+
7+
{/* DO NOT EDIT. The content of this doc is generated from the source above. To edit the content of this page, navigate to the source page in your editor. You can use the `<PagesOnly>Content</PagesOnly>` component to add content that is specific to the Pages Router. Any shared content should not be wrapped in a component. */}

packages/next/errors.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,5 +780,8 @@
780780
"779": "Route %s used \"searchParams\" inside \"use cache\". Accessing dynamic request data inside a cache scope is not supported. If you need some search params inside a cached function await \"searchParams\" outside of the cached function and pass only the required search params as arguments to the cached function. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache",
781781
"780": "Invariant: failed to find parent dynamic route for notFound route %s",
782782
"781": "No LRU node to remove",
783-
"782": "Failed to find the root directory of the project. This is a bug in Next.js."
783+
"782": "Failed to find the root directory of the project. This is a bug in Next.js.",
784+
"783": "Client Max Body Size must be a valid number (bytes) or filesize format string (e.g., \"5mb\")",
785+
"784": "Client Max Body Size must be larger than 0 bytes",
786+
"785": "Request body exceeded %s"
784787
}

packages/next/src/server/body-streams.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { IncomingMessage } from 'http'
22
import type { Readable } from 'stream'
33
import { PassThrough } from 'stream'
4+
import bytes from 'next/dist/compiled/bytes'
5+
6+
const DEFAULT_BODY_CLONE_SIZE_LIMIT = 10 * 1024 * 1024 // 10MB
47

58
export function requestToBodyStream(
69
context: { ReadableStream: typeof ReadableStream },
@@ -38,7 +41,8 @@ export interface CloneableBody {
3841
}
3942

4043
export function getCloneableBody<T extends IncomingMessage>(
41-
readable: T
44+
readable: T,
45+
sizeLimit?: number
4246
): CloneableBody {
4347
let buffered: Readable | null = null
4448

@@ -76,13 +80,35 @@ export function getCloneableBody<T extends IncomingMessage>(
7680
const input = buffered ?? readable
7781
const p1 = new PassThrough()
7882
const p2 = new PassThrough()
83+
84+
let bytesRead = 0
85+
const bodySizeLimit = sizeLimit ?? DEFAULT_BODY_CLONE_SIZE_LIMIT
86+
let limitExceeded = false
87+
7988
input.on('data', (chunk) => {
89+
if (limitExceeded) return
90+
91+
bytesRead += chunk.length
92+
93+
if (bytesRead > bodySizeLimit) {
94+
limitExceeded = true
95+
const urlInfo = readable.url ? ` for ${readable.url}` : ''
96+
console.warn(
97+
`Request body exceeded ${bytes.format(bodySizeLimit)}${urlInfo}. Only the first ${bytes.format(bodySizeLimit)} will be available unless configured. See https://nextjs.org/docs/app/api-reference/config/next-config-js/middlewareClientMaxBodySize for more details.`
98+
)
99+
p1.push(null)
100+
p2.push(null)
101+
return
102+
}
103+
80104
p1.push(chunk)
81105
p2.push(chunk)
82106
})
83107
input.on('end', () => {
84-
p1.push(null)
85-
p2.push(null)
108+
if (!limitExceeded) {
109+
p1.push(null)
110+
p2.push(null)
111+
}
86112
})
87113
buffered = p2
88114
return p1

packages/next/src/server/config-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
404404
linkNoTouchStart: z.boolean().optional(),
405405
manualClientBasePath: z.boolean().optional(),
406406
middlewarePrefetch: z.enum(['strict', 'flexible']).optional(),
407+
middlewareClientMaxBodySize: zSizeLimit.optional(),
407408
multiZoneDraftMode: z.boolean().optional(),
408409
cssChunking: z.union([z.boolean(), z.literal('strict')]).optional(),
409410
nextScriptWorkers: z.boolean().optional(),

packages/next/src/server/config-shared.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,12 @@ export interface ExperimentalConfig {
906906
* Enable accessing root params via the `next/root-params` module.
907907
*/
908908
rootParams?: boolean
909+
910+
/**
911+
* Body size limit for request bodies with middleware configured.
912+
* Defaults to 10MB. Can be specified as a number (bytes) or string (e.g. '5mb').
913+
*/
914+
middlewareClientMaxBodySize?: SizeLimit
909915
}
910916

911917
export type ExportPathMap = {
@@ -1614,6 +1620,7 @@ export const defaultConfig = Object.freeze({
16141620
devtoolSegmentExplorer: true,
16151621
browserDebugInfoInTerminal: false,
16161622
optimizeRouterScrolling: false,
1623+
middlewareClientMaxBodySize: 10_485_760, // 10MB
16171624
},
16181625
htmlLimitedBots: undefined,
16191626
bundlePagesRouterDependencies: false,

packages/next/src/server/config.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,32 @@ function assignDefaults(
718718
}
719719
}
720720

721+
// Normalize & validate experimental.middlewareClientMaxBodySize
722+
if (typeof result.experimental?.middlewareClientMaxBodySize !== 'undefined') {
723+
const middlewareClientMaxBodySize =
724+
result.experimental.middlewareClientMaxBodySize
725+
let normalizedValue: number
726+
727+
if (typeof middlewareClientMaxBodySize === 'string') {
728+
const bytes =
729+
require('next/dist/compiled/bytes') as typeof import('next/dist/compiled/bytes')
730+
normalizedValue = bytes.parse(middlewareClientMaxBodySize)
731+
} else if (typeof middlewareClientMaxBodySize === 'number') {
732+
normalizedValue = middlewareClientMaxBodySize
733+
} else {
734+
throw new Error(
735+
'Client Max Body Size must be a valid number (bytes) or filesize format string (e.g., "5mb")'
736+
)
737+
}
738+
739+
if (isNaN(normalizedValue) || normalizedValue < 1) {
740+
throw new Error('Client Max Body Size must be larger than 0 bytes')
741+
}
742+
743+
// Store the normalized value as a number
744+
result.experimental.middlewareClientMaxBodySize = normalizedValue
745+
}
746+
721747
warnOptionHasBeenMovedOutOfExperimental(
722748
result,
723749
'transpilePackages',

packages/next/src/server/lib/router-utils/resolve-routes.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ export function getResolveRoutes(
165165
addRequestMeta(req, 'initProtocol', protocol)
166166

167167
if (!isUpgradeReq) {
168-
addRequestMeta(req, 'clonableBody', getCloneableBody(req))
168+
const bodySizeLimit = config.experimental.middlewareClientMaxBodySize as
169+
| number
170+
| undefined
171+
addRequestMeta(req, 'clonableBody', getCloneableBody(req, bodySizeLimit))
169172
}
170173

171174
const maybeAddTrailingSlash = (pathname: string) => {

packages/next/src/server/next-server.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1925,7 +1925,13 @@ export default class NextNodeServer extends BaseServer<
19251925
addRequestMeta(req, 'initProtocol', protocol)
19261926

19271927
if (!isUpgradeReq) {
1928-
addRequestMeta(req, 'clonableBody', getCloneableBody(req.originalRequest))
1928+
const bodySizeLimit = this.nextConfig.experimental
1929+
?.middlewareClientMaxBodySize as number | undefined
1930+
addRequestMeta(
1931+
req,
1932+
'clonableBody',
1933+
getCloneableBody(req.originalRequest, bodySizeLimit)
1934+
)
19291935
}
19301936
}
19311937

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
3+
export async function POST(request: NextRequest) {
4+
const body = await request.text()
5+
return new NextResponse(
6+
JSON.stringify({
7+
message: 'Hello World',
8+
bodySize: body.length,
9+
}),
10+
{
11+
status: 200,
12+
headers: { 'Content-Type': 'application/json' },
13+
}
14+
)
15+
}

0 commit comments

Comments
 (0)