Skip to content

Commit 1caaca3

Browse files
styfleunstubbable
authored andcommitted
feat(next/image)!: add images.maximumResponseBody config (#88183)
Add `images.maximumResponseBody` configuration to exit early when attempting to optimize large source images.
1 parent 522ed84 commit 1caaca3

File tree

12 files changed

+264
-2
lines changed

12 files changed

+264
-2
lines changed

crates/next-build-test/nextConfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"minimumCacheTTL": 60,
3232
"formats": ["image/avif", "image/webp"],
3333
"maximumRedirects": 3,
34+
"maximumResponseBody": 300000000,
3435
"dangerouslyAllowLocalIP": false,
3536
"dangerouslyAllowSVG": false,
3637
"contentSecurityPolicy": "script-src 'none'; frame-src 'none'; sandbox;",

docs/01-app/03-api-reference/02-components/image.mdx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,28 @@ module.exports = {
837837
}
838838
```
839839

840+
#### `maximumResponseBody`
841+
842+
The default image optimization loader will fetch source images up to 300 MB in size.
843+
844+
```js filename="next.config.js"
845+
module.exports = {
846+
images: {
847+
maximumResponseBody: 300_000_000,
848+
},
849+
}
850+
```
851+
852+
If you know all your source images are small, you can protect memory constrained servers by reducing this to a smaller value such as 50 MB.
853+
854+
```js filename="next.config.js"
855+
module.exports = {
856+
images: {
857+
maximumResponseBody: 50_000_000,
858+
},
859+
}
860+
```
861+
840862
#### `dangerouslyAllowLocalIP`
841863

842864
In rare cases when self-hosting Next.js on a private network, you may want to allow optimizing images from local IP addresses on the same network. This is not recommended for most users because it could allow malicious users to access content on your internal network.
@@ -1341,6 +1363,7 @@ export default function Home() {
13411363

13421364
| Version | Changes |
13431365
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1366+
| `v16.1.2` | `maximumResponseBody` configuration added. |
13441367
| `v16.0.0` | `qualities` default configuration changed to `[75]`, `preload` prop added, `priority` prop deprecated, `dangerouslyAllowLocalIP` config added, `maximumRedirects` config added. |
13451368
| `v15.3.0` | `remotePatterns` added support for array of `URL` objects. |
13461369
| `v15.0.0` | `contentDispositionType` configuration default changed to `attachment`. |

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,12 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
586586
loader: z.enum(VALID_LOADERS).optional(),
587587
loaderFile: z.string().optional(),
588588
maximumRedirects: z.number().int().min(0).max(20).optional(),
589+
maximumResponseBody: z
590+
.number()
591+
.int()
592+
.min(1)
593+
.max(Number.MAX_SAFE_INTEGER)
594+
.optional(),
589595
minimumCacheTTL: z.number().int().gte(0).optional(),
590596
path: z.string().optional(),
591597
qualities: z

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@ function isRedirect(statusCode: number) {
711711
export async function fetchExternalImage(
712712
href: string,
713713
dangerouslyAllowLocalIP: boolean,
714+
maximumResponseBody: number,
714715
count = 3
715716
): Promise<ImageUpstream> {
716717
if (!dangerouslyAllowLocalIP) {
@@ -766,7 +767,12 @@ export async function fetchExternalImage(
766767
)
767768
}
768769
const redirect = new URL(locationHeader, href).href
769-
return fetchExternalImage(redirect, dangerouslyAllowLocalIP, count - 1)
770+
return fetchExternalImage(
771+
redirect,
772+
dangerouslyAllowLocalIP,
773+
maximumResponseBody,
774+
count - 1
775+
)
770776
}
771777

772778
if (!res.ok) {
@@ -777,7 +783,35 @@ export async function fetchExternalImage(
777783
)
778784
}
779785

780-
const buffer = Buffer.from(await res.arrayBuffer())
786+
if (!res.body) {
787+
Log.error('upstream image response is empty for', href)
788+
throw new ImageError(
789+
400,
790+
'"url" parameter is valid but upstream response is invalid'
791+
)
792+
}
793+
794+
const chunks: Buffer[] = []
795+
let totalSize = 0
796+
797+
for await (const c of res.body) {
798+
const chunk = Buffer.from(c)
799+
totalSize += chunk.byteLength
800+
if (totalSize > maximumResponseBody) {
801+
Log.error(
802+
'upstream image response exceeded maximum size for',
803+
href,
804+
totalSize
805+
)
806+
throw new ImageError(
807+
413,
808+
'"url" parameter is valid but upstream response is invalid'
809+
)
810+
}
811+
chunks.push(chunk)
812+
}
813+
814+
const buffer = Buffer.concat(chunks)
781815
const contentType = res.headers.get('Content-Type')
782816
const cacheControl = res.headers.get('Cache-Control')
783817
const etag = extractEtag(res.headers.get('ETag'), buffer)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,7 @@ export default class NextNodeServer extends BaseServer<
784784
? await fetchExternalImage(
785785
href,
786786
this.nextConfig.images.dangerouslyAllowLocalIP,
787+
this.nextConfig.images.maximumResponseBody,
787788
this.nextConfig.images.maximumRedirects
788789
)
789790
: await fetchInternalImage(

packages/next/src/shared/lib/image-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ export type ImageConfigComplete = {
106106
/** @see [Maximum Redirects](https://nextjs.org/docs/api-reference/next/image#maximumredirects) */
107107
maximumRedirects: number
108108

109+
/** @see [Maximum Response Body](https://nextjs.org/docs/api-reference/next/image#maximumresponsebody) */
110+
maximumResponseBody: number
111+
109112
/** @see [Dangerously Allow Local IP](https://nextjs.org/docs/api-reference/next/image#dangerously-allow-local-ip) */
110113
dangerouslyAllowLocalIP: boolean
111114

@@ -147,6 +150,7 @@ export const imageConfigDefault: ImageConfigComplete = {
147150
minimumCacheTTL: 14400, // 4 hours
148151
formats: ['image/webp'],
149152
maximumRedirects: 3,
153+
maximumResponseBody: 300_000_000, // 300MB
150154
dangerouslyAllowLocalIP: false,
151155
dangerouslyAllowSVG: false,
152156
contentSecurityPolicy: `script-src 'none'; frame-src 'none'; sandbox;`,

test/integration/next-image-new/app-dir-localpatterns/test/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ function runTests(mode: 'dev' | 'server') {
9898
},
9999
],
100100
maximumRedirects: 3,
101+
maximumResponseBody: 300000000,
101102
minimumCacheTTL: 14400,
102103
path: '/_next/image',
103104
qualities: [75],

test/integration/next-image-new/app-dir-qualities/test/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ function runTests(mode: 'dev' | 'server') {
110110
},
111111
],
112112
maximumRedirects: 3,
113+
maximumResponseBody: 300000000,
113114
minimumCacheTTL: 14400,
114115
path: '/_next/image',
115116
qualities: [42, 69, 88],

test/integration/next-image-new/app-dir/test/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,7 @@ function runTests(mode: 'dev' | 'server') {
17961796
},
17971797
],
17981798
maximumRedirects: 3,
1799+
maximumResponseBody: 300000000,
17991800
minimumCacheTTL: 14400,
18001801
path: '/_next/image',
18011802
qualities: [75],

test/integration/next-image-new/unicode/test/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ function runTests(mode: 'server' | 'dev') {
103103
},
104104
],
105105
maximumRedirects: 3,
106+
maximumResponseBody: 300000000,
106107
minimumCacheTTL: 14400,
107108
path: '/_next/image',
108109
qualities: [75],

0 commit comments

Comments
 (0)