Skip to content

Commit f436714

Browse files
committed
feat(next/image): support new URL() for images.remotePatterns
1 parent 534bfd7 commit f436714

File tree

13 files changed

+442
-24
lines changed

13 files changed

+442
-24
lines changed

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

+11
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,16 @@ module.exports = {
520520

521521
To protect your application from malicious users, configuration is required in order to use external images. This ensures that only external images from your account can be served from the Next.js Image Optimization API. These external images can be configured with the `remotePatterns` property in your `next.config.js` file, as shown below:
522522

523+
```js filename="next.config.js"
524+
module.exports = {
525+
images: {
526+
remotePatterns: [new URL('https://example.com/account123/**')],
527+
},
528+
}
529+
```
530+
531+
Versions of Next.js prior to 15.3.0 can configure `remotePatterns` using the object:
532+
523533
```js filename="next.config.js"
524534
module.exports = {
525535
images: {
@@ -1106,6 +1116,7 @@ This `next/image` component uses browser native [lazy loading](https://caniuse.c
11061116

11071117
| Version | Changes |
11081118
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1119+
| `v15.3.0` | `remotePatterns` added support for array of `URL` objects. |
11091120
| `v15.0.0` | `contentDispositionType` configuration default changed to `attachment`. |
11101121
| `v14.2.23` | `qualities` configuration added. |
11111122
| `v14.2.15` | `decoding` prop added and `localPatterns` configuration added. |

errors/next-image-unconfigured-host.mdx

+16-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ One of your pages that leverages the `next/image` component, passed a `src` valu
1010

1111
Add the protocol, hostname, port, and pathname to the `images.remotePatterns` config in `next.config.js`:
1212

13+
```js filename="next.config.js"
14+
module.exports = {
15+
images: {
16+
remotePatterns: [new URL('https://assets.example.com/account123/**')],
17+
},
18+
}
19+
```
20+
21+
### Fixing older versions of Next.js
22+
23+
<details>
24+
<summary>Using Next.js prior to 15.3.0?</summary>
25+
26+
Older versions of Next.js can configure `images.remotePatterns` using the object:
27+
1328
```js filename="next.config.js"
1429
module.exports = {
1530
images: {
@@ -26,7 +41,7 @@ module.exports = {
2641
}
2742
```
2843

29-
### Fixing older versions of Next.js
44+
</details>
3045

3146
<details>
3247
<summary>Using Next.js prior to 12.3.0?</summary>

packages/next/errors.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -666,5 +666,6 @@
666666
"665": "Failed to find Server Action \"%s\". This request might be from an older or newer deployment.\\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action",
667667
"666": "Turbopack builds are only available in canary builds of Next.js.",
668668
"667": "receiveExpiredTags is deprecated, and not expected to be called.",
669-
"668": "Internal Next.js error: Router action dispatched before initialization."
669+
"668": "Internal Next.js error: Router action dispatched before initialization.",
670+
"669": "Specified images.remotePatterns must have protocol \"http\" or \"https\" received \"%s\"."
670671
}

packages/next/src/build/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ async function writeImagesManifest(
582582
// By default, remotePatterns will allow no remote images ([])
583583
images.remotePatterns = (config?.images?.remotePatterns || []).map((p) => ({
584584
// Modifying the manifest should also modify matchRemotePattern()
585-
protocol: p.protocol,
585+
protocol: p.protocol?.replace(/:$/, '') as 'http' | 'https' | undefined,
586586
hostname: makeRe(p.hostname).source,
587587
port: p.port,
588588
pathname: makeRe(p.pathname ?? '**', { dot: true }).source,

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

+10-7
Original file line numberDiff line numberDiff line change
@@ -525,13 +525,16 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
525525
.optional(),
526526
remotePatterns: z
527527
.array(
528-
z.strictObject({
529-
hostname: z.string(),
530-
pathname: z.string().optional(),
531-
port: z.string().max(5).optional(),
532-
protocol: z.enum(['http', 'https']).optional(),
533-
search: z.string().optional(),
534-
})
528+
z.union([
529+
z.instanceof(URL),
530+
z.strictObject({
531+
hostname: z.string(),
532+
pathname: z.string().optional(),
533+
port: z.string().max(5).optional(),
534+
protocol: z.enum(['http', 'https']).optional(),
535+
search: z.string().optional(),
536+
}),
537+
])
535538
)
536539
.max(50)
537540
.optional(),

packages/next/src/server/config.ts

+24
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,30 @@ function assignDefaults(
364364
)
365365
}
366366

367+
// Convert URL to RemotePattern
368+
images.remotePatterns = images.remotePatterns.map(
369+
({ protocol, hostname, port, pathname, search }) => {
370+
if (
371+
protocol &&
372+
!['http', 'https', 'http:', 'https:'].includes(protocol)
373+
) {
374+
throw new Error(
375+
`Specified images.remotePatterns must have protocol "http" or "https" received "${protocol}".`
376+
)
377+
}
378+
return {
379+
protocol: protocol?.replace(/:$/, '') as
380+
| 'http'
381+
| 'https'
382+
| undefined,
383+
hostname,
384+
port,
385+
pathname,
386+
search,
387+
}
388+
}
389+
)
390+
367391
// static images are automatically prefixed with assetPrefix
368392
// so we need to ensure _next/image allows downloading from
369393
// this resource

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export type ImageConfigComplete = {
113113
contentDispositionType: 'inline' | 'attachment'
114114

115115
/** @see [Remote Patterns](https://nextjs.org/docs/api-reference/next/image#remotepatterns) */
116-
remotePatterns: RemotePattern[]
116+
remotePatterns: Array<URL | RemotePattern>
117117

118118
/** @see [Remote Patterns](https://nextjs.org/docs/api-reference/next/image#localPatterns) */
119119
localPatterns: LocalPattern[] | undefined

packages/next/src/shared/lib/match-remote-pattern.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import type { RemotePattern } from './image-config'
22
import { makeRe } from 'next/dist/compiled/picomatch'
33

44
// Modifying this function should also modify writeImagesManifest()
5-
export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean {
5+
export function matchRemotePattern(
6+
pattern: RemotePattern | URL,
7+
url: URL
8+
): boolean {
69
if (pattern.protocol !== undefined) {
7-
const actualProto = url.protocol.slice(0, -1)
8-
if (pattern.protocol !== actualProto) {
10+
if (pattern.protocol.replace(/:$/, '') !== url.protocol.replace(/:$/, '')) {
911
return false
1012
}
1113
}
@@ -41,7 +43,7 @@ export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean {
4143

4244
export function hasRemoteMatch(
4345
domains: string[],
44-
remotePatterns: RemotePattern[],
46+
remotePatterns: Array<RemotePattern | URL>,
4547
url: URL
4648
): boolean {
4749
return (

test/e2e/app-dir/next-image/next.config.js

-5
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { NextConfig } from 'next'
2+
3+
const nextConfig: NextConfig = {
4+
images: {
5+
remotePatterns: [new URL('https://image-optimization-test.vercel.app/**')],
6+
},
7+
}
8+
9+
export default nextConfig
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module.exports = {
22
images: {
3-
remotePatterns: [{ hostname: 'image-optimization-test.vercel.app' }],
3+
remotePatterns: [new URL('https://image-optimization-test.vercel.app/**')],
44
},
55
}

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

+43-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import {
44
findPort,
5+
getImagesManifest,
56
killApp,
67
launchApp,
78
nextBuild,
@@ -17,7 +18,7 @@ let appPort
1718
let app
1819
let browser
1920

20-
function runTests() {
21+
function runTests(mode: 'server' | 'dev') {
2122
it('should load static unicode image', async () => {
2223
const src = await browser.elementById('static').getAttribute('src')
2324
expect(src).toMatch(
@@ -65,6 +66,45 @@ function runTests() {
6566
const res = await fetch(fullSrc)
6667
expect(res.status).toBe(200)
6768
})
69+
if (mode === 'server') {
70+
it('should build correct images-manifest.json', async () => {
71+
const manifest = getImagesManifest(appDir)
72+
expect(manifest).toEqual({
73+
version: 1,
74+
images: {
75+
contentDispositionType: 'attachment',
76+
contentSecurityPolicy:
77+
"script-src 'none'; frame-src 'none'; sandbox;",
78+
dangerouslyAllowSVG: false,
79+
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
80+
disableStaticImages: false,
81+
domains: [],
82+
formats: ['image/webp'],
83+
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
84+
loader: 'default',
85+
loaderFile: '',
86+
remotePatterns: [
87+
{
88+
protocol: 'https',
89+
hostname:
90+
'^(?:^(?:image\\-optimization\\-test\\.vercel\\.app)$)$',
91+
port: '',
92+
pathname:
93+
'^(?:\\/(?!\\.{1,2}(?:\\/|$))(?:(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$)).)*?))$',
94+
search: '',
95+
},
96+
],
97+
minimumCacheTTL: 60,
98+
path: '/_next/image',
99+
sizes: [
100+
640, 750, 828, 1080, 1200, 1920, 2048, 3840, 16, 32, 48, 64, 96,
101+
128, 256, 384,
102+
],
103+
unoptimized: false,
104+
},
105+
})
106+
})
107+
}
68108
}
69109

70110
describe('Image Component Unicode Image URL', () => {
@@ -82,7 +122,7 @@ describe('Image Component Unicode Image URL', () => {
82122
browser.close()
83123
}
84124
})
85-
runTests()
125+
runTests('dev')
86126
}
87127
)
88128
;(process.env.TURBOPACK_DEV ? describe.skip : describe)(
@@ -100,7 +140,7 @@ describe('Image Component Unicode Image URL', () => {
100140
browser.close()
101141
}
102142
})
103-
runTests()
143+
runTests('server')
104144
}
105145
)
106146
})

0 commit comments

Comments
 (0)