Repository: vercel/next.js
Pull request: feat(next/image)!: deprecate and warn on `images.domains` config

This was docs deprecated in Next.js 14 so we should begin warning in Next.js 16 when `images.domains` config is used.

This will guide users to providing a strict `images.remotePatterns` config ensuring only expected images can be optimized.

Changed files:
filename: docs/01-app/03-api-reference/02-components/image.mdx
patch: @@ -871,10 +871,12 @@ You can optionally configure `inline` to allow the browser to render the image w
 
 #### `domains`
 
-> **Warning**: Deprecated since Next.js 14 in favor of strict [`remotePatterns`](#remotepatterns) in order to protect your application from malicious users. Only use `domains` if you own all the content served from the domain.
+> **Warning**: Deprecated since Next.js 14 in favor of strict [`remotePatterns`](#remotepatterns) in order to protect your application from malicious users.
 
 Similar to [`remotePatterns`](#remotepatterns), the `domains` configuration can be used to provide a list of allowed hostnames for external images. However, the `domains` configuration does not support wildcard pattern matching and it cannot restrict protocol, port, or pathname.
 
+Since most remote image servers are shared between multiple tenants, it's safer to use `remotePatterns` to ensure only the intended images are optimized.
+
 Below is an example of the `domains` property in the `next.config.js` file:
 
 ```js filename="next.config.js"

filename: packages/next/src/server/config.ts
patch: @@ -120,6 +120,15 @@ function checkDeprecations(
     silent
   )
 
+  if (userConfig.images?.domains?.length) {
+    warnOptionHasBeenDeprecated(
+      userConfig,
+      'images.domains',
+      `\`images.domains\` is deprecated in favor of \`images.remotePatterns\`. Please update ${configFileName} to protect your application from malicious users.`,
+      silent
+    )
+  }
+
   // i18n deprecation for App Router
   if (userConfig.i18n) {
     const hasAppDir = Boolean(findDir(dir, 'app'))

filename: packages/next/src/shared/lib/image-config.ts
patch: @@ -133,6 +133,9 @@ export const imageConfigDefault: ImageConfigComplete = {
   path: '/_next/image',
   loader: 'default',
   loaderFile: '',
+  /**
+   * @deprecated Use `remotePatterns` instead to protect your application from malicious users.
+   */
   domains: [],
   disableStaticImages: false,
   minimumCacheTTL: 14400, // 4 hours

filename: test/integration/next-image-new/image-from-node-modules/test/index.test.ts
patch: @@ -13,7 +13,7 @@ const appDir = join(__dirname, '../')
 let appPort
 let app
 
-function runTests() {
+function runTests(getOutput: () => string) {
   it('should apply image config for node_modules', async () => {
     const browser = await webdriver(appPort, '/')
     const src = await browser
@@ -26,34 +26,49 @@ function runTests() {
       .getAttribute('srcset')
     expect(srcset).toMatch('1234')
   })
+
+  it('should warn when using images.domains config', async () => {
+    expect(getOutput()).toContain(
+      '`images.domains` is deprecated in favor of `images.remotePatterns`. Please update next.config.js to protect your application from malicious users.'
+    )
+  })
 }
 
 describe('Image Component from node_modules prod mode', () => {
   ;(process.env.TURBOPACK_DEV ? describe.skip : describe)(
     'production mode',
     () => {
+      let output = ''
       beforeAll(async () => {
-        await nextBuild(appDir)
+        const result = await nextBuild(appDir, [], {
+          stderr: true,
+          stdout: true,
+        })
+        output = (result.stderr ?? '') + (result.stdout ?? '')
         appPort = await findPort()
         app = await nextStart(appDir, appPort)
       })
       afterAll(async () => {
         await killApp(app)
       })
 
-      runTests()
+      runTests(() => output)
     }
   )
 })
 
 describe('Image Component from node_modules development mode', () => {
+  let output = ''
   beforeAll(async () => {
     appPort = await findPort()
-    app = await launchApp(appDir, appPort)
+    app = await launchApp(appDir, appPort, {
+      onStderr: (msg) => (output += msg),
+      onStdout: (msg) => (output += msg),
+    })
   })
   afterAll(async () => {
     await killApp(app)
   })
 
-  runTests()
+  runTests(() => output)
 })