Skip to content
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(next/image): support new URL() for images.remotePatterns #77692

Merged
merged 3 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/01-app/04-api-reference/02-components/image.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,16 @@ module.exports = {

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:

```js filename="next.config.js"
module.exports = {
images: {
remotePatterns: [new URL('https://example.com/account123/**')],
},
}
```

Versions of Next.js prior to 15.3.0 can configure `remotePatterns` using the object:

```js filename="next.config.js"
module.exports = {
images: {
Expand Down Expand Up @@ -1106,6 +1116,7 @@ This `next/image` component uses browser native [lazy loading](https://caniuse.c

| Version | Changes |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `v15.3.0` | `remotePatterns` added support for array of `URL` objects. |
| `v15.0.0` | `contentDispositionType` configuration default changed to `attachment`. |
| `v14.2.23` | `qualities` configuration added. |
| `v14.2.15` | `decoding` prop added and `localPatterns` configuration added. |
Expand Down
17 changes: 16 additions & 1 deletion errors/next-image-unconfigured-host.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ One of your pages that leverages the `next/image` component, passed a `src` valu

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

```js filename="next.config.js"
module.exports = {
images: {
remotePatterns: [new URL('https://assets.example.com/account123/**')],
},
}
```

### Fixing older versions of Next.js

<details>
<summary>Using Next.js prior to 15.3.0?</summary>

Older versions of Next.js can configure `images.remotePatterns` using the object:

```js filename="next.config.js"
module.exports = {
images: {
Expand All @@ -26,7 +41,7 @@ module.exports = {
}
```

### Fixing older versions of Next.js
</details>

<details>
<summary>Using Next.js prior to 12.3.0?</summary>
Expand Down
3 changes: 2 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -666,5 +666,6 @@
"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",
"666": "Turbopack builds are only available in canary builds of Next.js.",
"667": "receiveExpiredTags is deprecated, and not expected to be called.",
"668": "Internal Next.js error: Router action dispatched before initialization."
"668": "Internal Next.js error: Router action dispatched before initialization.",
"669": "Specified images.remotePatterns must have protocol \"http\" or \"https\" received \"%s\"."
}
2 changes: 1 addition & 1 deletion packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ async function writeImagesManifest(
// By default, remotePatterns will allow no remote images ([])
images.remotePatterns = (config?.images?.remotePatterns || []).map((p) => ({
// Modifying the manifest should also modify matchRemotePattern()
protocol: p.protocol,
protocol: p.protocol?.replace(/:$/, '') as 'http' | 'https' | undefined,
hostname: makeRe(p.hostname).source,
port: p.port,
pathname: makeRe(p.pathname ?? '**', { dot: true }).source,
Expand Down
17 changes: 10 additions & 7 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,13 +525,16 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
.optional(),
remotePatterns: z
.array(
z.strictObject({
hostname: z.string(),
pathname: z.string().optional(),
port: z.string().max(5).optional(),
protocol: z.enum(['http', 'https']).optional(),
search: z.string().optional(),
})
z.union([
z.instanceof(URL),
z.strictObject({
hostname: z.string(),
pathname: z.string().optional(),
port: z.string().max(5).optional(),
protocol: z.enum(['http', 'https']).optional(),
search: z.string().optional(),
}),
])
)
.max(50)
.optional(),
Expand Down
24 changes: 24 additions & 0 deletions packages/next/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,30 @@ function assignDefaults(
)
}

// Convert URL to RemotePattern
images.remotePatterns = images.remotePatterns.map(
({ protocol, hostname, port, pathname, search }) => {
if (
protocol &&
!['http', 'https', 'http:', 'https:'].includes(protocol)
) {
throw new Error(
`Specified images.remotePatterns must have protocol "http" or "https" received "${protocol}".`
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test for that error? Curious about the stack.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eps1lon Added in 754c194

}
return {
protocol: protocol?.replace(/:$/, '') as
| 'http'
| 'https'
| undefined,
hostname,
port,
pathname,
search,
}
}
)

// static images are automatically prefixed with assetPrefix
// so we need to ensure _next/image allows downloading from
// this resource
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/shared/lib/image-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export type ImageConfigComplete = {
contentDispositionType: 'inline' | 'attachment'

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

/** @see [Remote Patterns](https://nextjs.org/docs/api-reference/next/image#localPatterns) */
localPatterns: LocalPattern[] | undefined
Expand Down
10 changes: 6 additions & 4 deletions packages/next/src/shared/lib/match-remote-pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import type { RemotePattern } from './image-config'
import { makeRe } from 'next/dist/compiled/picomatch'

// Modifying this function should also modify writeImagesManifest()
export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean {
export function matchRemotePattern(
pattern: RemotePattern | URL,
url: URL
): boolean {
if (pattern.protocol !== undefined) {
const actualProto = url.protocol.slice(0, -1)
if (pattern.protocol !== actualProto) {
if (pattern.protocol.replace(/:$/, '') !== url.protocol.replace(/:$/, '')) {
return false
}
}
Expand Down Expand Up @@ -41,7 +43,7 @@ export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean {

export function hasRemoteMatch(
domains: string[],
remotePatterns: RemotePattern[],
remotePatterns: Array<RemotePattern | URL>,
url: URL
): boolean {
return (
Expand Down
5 changes: 0 additions & 5 deletions test/e2e/app-dir/next-image/next.config.js

This file was deleted.

9 changes: 9 additions & 0 deletions test/e2e/app-dir/next-image/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
images: {
remotePatterns: [new URL('https://image-optimization-test.vercel.app/**')],
},
}

export default nextConfig
2 changes: 1 addition & 1 deletion test/integration/next-image-new/unicode/next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
images: {
remotePatterns: [{ hostname: 'image-optimization-test.vercel.app' }],
remotePatterns: [new URL('https://image-optimization-test.vercel.app/**')],
},
}
46 changes: 43 additions & 3 deletions test/integration/next-image-new/unicode/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {
findPort,
getImagesManifest,
killApp,
launchApp,
nextBuild,
Expand All @@ -17,7 +18,7 @@ let appPort
let app
let browser

function runTests() {
function runTests(mode: 'server' | 'dev') {
it('should load static unicode image', async () => {
const src = await browser.elementById('static').getAttribute('src')
expect(src).toMatch(
Expand Down Expand Up @@ -65,6 +66,45 @@ function runTests() {
const res = await fetch(fullSrc)
expect(res.status).toBe(200)
})
if (mode === 'server') {
it('should build correct images-manifest.json', async () => {
const manifest = getImagesManifest(appDir)
expect(manifest).toEqual({
version: 1,
images: {
contentDispositionType: 'attachment',
contentSecurityPolicy:
"script-src 'none'; frame-src 'none'; sandbox;",
dangerouslyAllowSVG: false,
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
disableStaticImages: false,
domains: [],
formats: ['image/webp'],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
loader: 'default',
loaderFile: '',
remotePatterns: [
{
protocol: 'https',
hostname:
'^(?:^(?:image\\-optimization\\-test\\.vercel\\.app)$)$',
port: '',
pathname:
'^(?:\\/(?!\\.{1,2}(?:\\/|$))(?:(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$)).)*?))$',
search: '',
},
],
minimumCacheTTL: 60,
path: '/_next/image',
sizes: [
640, 750, 828, 1080, 1200, 1920, 2048, 3840, 16, 32, 48, 64, 96,
128, 256, 384,
],
unoptimized: false,
},
})
})
}
}

describe('Image Component Unicode Image URL', () => {
Expand All @@ -82,7 +122,7 @@ describe('Image Component Unicode Image URL', () => {
browser.close()
}
})
runTests()
runTests('dev')
}
)
;(process.env.TURBOPACK_DEV ? describe.skip : describe)(
Expand All @@ -100,7 +140,7 @@ describe('Image Component Unicode Image URL', () => {
browser.close()
}
})
runTests()
runTests('server')
}
)
})
Loading
Loading