Skip to content

Commit 17e6873

Browse files
ztannerijjk
andauthored
[backport]: experimental.middlewareClientMaxBodySize (#84722)
Backports: - #84539 - #84712 --------- Co-authored-by: JJ Kasper <[email protected]>
1 parent 4da39f2 commit 17e6873

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
@@ -717,5 +717,8 @@
717717
"716": "NEXT_DEVTOOLS_SIMULATED_ERROR",
718718
"717": "Unsupported environment condition \"%s\" and react condition \"%s\". This is a bug in Next.js.",
719719
"718": "Invariant: projectDir is required for node runtime",
720-
"719": "Failed to get source map for '%s'. This is a bug in Next.js"
720+
"719": "Failed to get source map for '%s'. This is a bug in Next.js",
721+
"720": "Client Max Body Size must be a valid number (bytes) or filesize format string (e.g., \"5mb\")",
722+
"721": "Client Max Body Size must be larger than 0 bytes",
723+
"722": "Request body exceeded %s"
721724
}

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
@@ -389,6 +389,7 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
389389
linkNoTouchStart: z.boolean().optional(),
390390
manualClientBasePath: z.boolean().optional(),
391391
middlewarePrefetch: z.enum(['strict', 'flexible']).optional(),
392+
middlewareClientMaxBodySize: zSizeLimit.optional(),
392393
multiZoneDraftMode: z.boolean().optional(),
393394
cssChunking: z.union([z.boolean(), z.literal('strict')]).optional(),
394395
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
@@ -777,6 +777,12 @@ export interface ExperimentalConfig {
777777
* @default false
778778
*/
779779
optimizeRouterScrolling?: boolean
780+
781+
/**
782+
* Body size limit for request bodies with middleware configured.
783+
* Defaults to 10MB. Can be specified as a number (bytes) or string (e.g. '5mb').
784+
*/
785+
middlewareClientMaxBodySize?: SizeLimit
780786
}
781787

782788
export type ExportPathMap = {
@@ -1501,6 +1507,7 @@ export const defaultConfig = {
15011507
browserDebugInfoInTerminal: false,
15021508
optimizeRouterScrolling: false,
15031509
strictNextHead: true,
1510+
middlewareClientMaxBodySize: 10_485_760, // 10MB
15041511
},
15051512
htmlLimitedBots: undefined,
15061513
bundlePagesRouterDependencies: false,

packages/next/src/server/config.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,32 @@ function assignDefaults(
658658
}
659659
}
660660

661+
// Normalize & validate experimental.middlewareClientMaxBodySize
662+
if (typeof result.experimental?.middlewareClientMaxBodySize !== 'undefined') {
663+
const middlewareClientMaxBodySize =
664+
result.experimental.middlewareClientMaxBodySize
665+
let normalizedValue: number
666+
667+
if (typeof middlewareClientMaxBodySize === 'string') {
668+
const bytes =
669+
require('next/dist/compiled/bytes') as typeof import('next/dist/compiled/bytes')
670+
normalizedValue = bytes.parse(middlewareClientMaxBodySize)
671+
} else if (typeof middlewareClientMaxBodySize === 'number') {
672+
normalizedValue = middlewareClientMaxBodySize
673+
} else {
674+
throw new Error(
675+
'Client Max Body Size must be a valid number (bytes) or filesize format string (e.g., "5mb")'
676+
)
677+
}
678+
679+
if (isNaN(normalizedValue) || normalizedValue < 1) {
680+
throw new Error('Client Max Body Size must be larger than 0 bytes')
681+
}
682+
683+
// Store the normalized value as a number
684+
result.experimental.middlewareClientMaxBodySize = normalizedValue
685+
}
686+
661687
warnOptionHasBeenMovedOutOfExperimental(
662688
result,
663689
'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
@@ -166,7 +166,10 @@ export function getResolveRoutes(
166166
addRequestMeta(req, 'initProtocol', protocol)
167167

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

172175
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
@@ -1930,7 +1930,13 @@ export default class NextNodeServer extends BaseServer<
19301930
addRequestMeta(req, 'initProtocol', protocol)
19311931

19321932
if (!isUpgradeReq) {
1933-
addRequestMeta(req, 'clonableBody', getCloneableBody(req.originalRequest))
1933+
const bodySizeLimit = this.nextConfig.experimental
1934+
?.middlewareClientMaxBodySize as number | undefined
1935+
addRequestMeta(
1936+
req,
1937+
'clonableBody',
1938+
getCloneableBody(req.originalRequest, bodySizeLimit)
1939+
)
19341940
}
19351941
}
19361942

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)