Repository: vercel/next.js
Pull request: fix(next/image): add `?dpl` query string for local images (without static import)

In a previous PR https://github.com/vercel/next.js/pull/73184 the `?dpl` query string was added to local images from static imports but not to images without the static import.

This was a subtle change based on malte's PR review comment here: https://github.com/vercel/next.js/pull/73184#issuecomment-2499126751

However, I can't think of reason why we should differentiate these two since they are both related to the current deployment (as opposed to remote images which live outside the deployment).

Furthermore, this string matching is causing microfrontends to fail to add the `?dpl` because they have a path prefix and thus fail skew protection.

So this PR fixes both problems by loosening the string matching to look for src images that start with `/`.

Furthermore, as sokra pointed out, we were not adding `?dpl` when `unoptimized=true` so this PR also fixes that issue.

Changed files:
filename: packages/next/src/shared/lib/get-img-props.ts
patch: @@ -1,4 +1,5 @@
 import { warnOnce } from './utils/warn-once'
+import { getDeploymentId } from './deployment-id'
 import { getImageBlurSvg } from './image-blur-svg'
 import { imageConfigDefault } from './image-config'
 import type {
@@ -230,6 +231,11 @@ function generateImgAttrs({
   loader,
 }: GenImgAttrsData): GenImgAttrsResult {
   if (unoptimized) {
+    const deploymentId = getDeploymentId()
+    if (src.startsWith('/') && deploymentId) {
+      const sep = src.includes('?') ? '&' : '?'
+      src = `${src}${sep}dpl=${deploymentId}`
+    }
     return { src, srcSet: undefined, sizes: undefined }
   }
 

filename: packages/next/src/shared/lib/image-loader.ts
patch: @@ -97,9 +97,7 @@ function defaultLoader({
 
   let deploymentId = getDeploymentId()
   return `${config.path}?url=${encodeURIComponent(src)}&w=${width}&q=${q}${
-    src.startsWith('/_next/static/media/') && deploymentId
-      ? `&dpl=${deploymentId}`
-      : ''
+    src.startsWith('/') && deploymentId ? `&dpl=${deploymentId}` : ''
   }`
 }
 

filename: test/unit/next-image-get-img-props.test.ts
patch: @@ -640,7 +640,37 @@ describe('getImageProps()', () => {
       delete process.env.NEXT_DEPLOYMENT_ID
     }
   })
-  it('should not add query string for relative local image when NEXT_DEPLOYMENT_ID defined', async () => {
+  it('should add query string for imported local image from microfrontend when NEXT_DEPLOYMENT_ID defined', async () => {
+    try {
+      process.env.NEXT_DEPLOYMENT_ID = 'dpl_123'
+      const { props } = getImageProps({
+        alt: 'a nice desc',
+        src: '/microfrontend/_next/static/media/test.abc123.png', // simulating microfrontend path
+        width: 100,
+        height: 200,
+      })
+      expect(warningMessages).toStrictEqual([])
+      expect(Object.entries(props)).toStrictEqual([
+        ['alt', 'a nice desc'],
+        ['loading', 'lazy'],
+        ['width', 100],
+        ['height', 200],
+        ['decoding', 'async'],
+        ['style', { color: 'transparent' }],
+        [
+          'srcSet',
+          '/_next/image?url=%2Fmicrofrontend%2F_next%2Fstatic%2Fmedia%2Ftest.abc123.png&w=128&q=75&dpl=dpl_123 1x, /_next/image?url=%2Fmicrofrontend%2F_next%2Fstatic%2Fmedia%2Ftest.abc123.png&w=256&q=75&dpl=dpl_123 2x',
+        ],
+        [
+          'src',
+          '/_next/image?url=%2Fmicrofrontend%2F_next%2Fstatic%2Fmedia%2Ftest.abc123.png&w=256&q=75&dpl=dpl_123',
+        ],
+      ])
+    } finally {
+      delete process.env.NEXT_DEPLOYMENT_ID
+    }
+  })
+  it('should add query string for relative local image when NEXT_DEPLOYMENT_ID defined', async () => {
     try {
       process.env.NEXT_DEPLOYMENT_ID = 'dpl_123'
       const { props } = getImageProps({
@@ -659,9 +689,9 @@ describe('getImageProps()', () => {
         ['style', { color: 'transparent' }],
         [
           'srcSet',
-          '/_next/image?url=%2Ftest.png&w=128&q=75 1x, /_next/image?url=%2Ftest.png&w=256&q=75 2x',
+          '/_next/image?url=%2Ftest.png&w=128&q=75&dpl=dpl_123 1x, /_next/image?url=%2Ftest.png&w=256&q=75&dpl=dpl_123 2x',
         ],
-        ['src', '/_next/image?url=%2Ftest.png&w=256&q=75'],
+        ['src', '/_next/image?url=%2Ftest.png&w=256&q=75&dpl=dpl_123'],
       ])
     } finally {
       delete process.env.NEXT_DEPLOYMENT_ID
@@ -697,4 +727,73 @@ describe('getImageProps()', () => {
       delete process.env.NEXT_DEPLOYMENT_ID
     }
   })
+  it('should add query string with question mark for unoptimized relative svg when NEXT_DEPLOYMENT_ID defined', async () => {
+    try {
+      process.env.NEXT_DEPLOYMENT_ID = 'dpl_123'
+      const { props } = getImageProps({
+        alt: 'a nice desc',
+        src: '/test.svg',
+        width: 100,
+        height: 200,
+      })
+      expect(warningMessages).toStrictEqual([])
+      expect(Object.entries(props)).toStrictEqual([
+        ['alt', 'a nice desc'],
+        ['loading', 'lazy'],
+        ['width', 100],
+        ['height', 200],
+        ['decoding', 'async'],
+        ['style', { color: 'transparent' }],
+        ['src', '/test.svg?dpl=dpl_123'],
+      ])
+    } finally {
+      delete process.env.NEXT_DEPLOYMENT_ID
+    }
+  })
+  it('should add query string with ampersand for unoptimized relative svg when NEXT_DEPLOYMENT_ID defined', async () => {
+    try {
+      process.env.NEXT_DEPLOYMENT_ID = 'dpl_123'
+      const { props } = getImageProps({
+        alt: 'a nice desc',
+        src: '/test.svg?v=1',
+        width: 100,
+        height: 200,
+      })
+      expect(warningMessages).toStrictEqual([])
+      expect(Object.entries(props)).toStrictEqual([
+        ['alt', 'a nice desc'],
+        ['loading', 'lazy'],
+        ['width', 100],
+        ['height', 200],
+        ['decoding', 'async'],
+        ['style', { color: 'transparent' }],
+        ['src', '/test.svg?v=1&dpl=dpl_123'],
+      ])
+    } finally {
+      delete process.env.NEXT_DEPLOYMENT_ID
+    }
+  })
+  it('should not add query string for unoptimized absolute remote svg when NEXT_DEPLOYMENT_ID defined', async () => {
+    try {
+      process.env.NEXT_DEPLOYMENT_ID = 'dpl_123'
+      const { props } = getImageProps({
+        alt: 'a nice desc',
+        src: 'http://example.com/test.svg',
+        width: 100,
+        height: 200,
+      })
+      expect(warningMessages).toStrictEqual([])
+      expect(Object.entries(props)).toStrictEqual([
+        ['alt', 'a nice desc'],
+        ['loading', 'lazy'],
+        ['width', 100],
+        ['height', 200],
+        ['decoding', 'async'],
+        ['style', { color: 'transparent' }],
+        ['src', 'http://example.com/test.svg'],
+      ])
+    } finally {
+      delete process.env.NEXT_DEPLOYMENT_ID
+    }
+  })
 })