From d575430c59839c33994357e57e8352dcb8a5c3b6 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Wed, 8 Oct 2025 15:42:23 +0200 Subject: [PATCH 1/5] Modify warnings to throw errors --- packages/next/src/build/templates/app-page.ts | 1 + .../next/src/build/templates/edge-ssr-app.ts | 1 + packages/next/src/client/image-component.tsx | 17 +++++- packages/next/src/client/legacy/image.tsx | 52 ++++++++++++------- .../next/src/server/app-render/app-render.tsx | 34 +++++++++--- packages/next/src/server/app-render/types.ts | 2 + packages/next/src/server/config.ts | 8 +++ packages/next/src/shared/lib/get-img-props.ts | 12 ----- packages/next/src/shared/lib/image-loader.ts | 13 +++++ test/unit/next-image-get-img-props.test.ts | 20 ------- 10 files changed, 100 insertions(+), 60 deletions(-) diff --git a/packages/next/src/build/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts index 467a8147e0755..2f87dad4ef297 100644 --- a/packages/next/src/build/templates/app-page.ts +++ b/packages/next/src/build/templates/app-page.ts @@ -546,6 +546,7 @@ export async function handler( nextConfigOutput: nextConfig.output, crossOrigin: nextConfig.crossOrigin, trailingSlash: nextConfig.trailingSlash, + images: nextConfig.images, previewProps: prerenderManifest.preview, deploymentId: nextConfig.deploymentId, enableTainting: nextConfig.experimental.taint, diff --git a/packages/next/src/build/templates/edge-ssr-app.ts b/packages/next/src/build/templates/edge-ssr-app.ts index 5138369286416..0bfdb850dc673 100644 --- a/packages/next/src/build/templates/edge-ssr-app.ts +++ b/packages/next/src/build/templates/edge-ssr-app.ts @@ -144,6 +144,7 @@ async function requestHandler( nextConfigOutput: nextConfig.output, crossOrigin: nextConfig.crossOrigin, trailingSlash: nextConfig.trailingSlash, + images: nextConfig.images, previewProps: prerenderManifest.preview, deploymentId: nextConfig.deploymentId, enableTainting: nextConfig.experimental.taint, diff --git a/packages/next/src/client/image-component.tsx b/packages/next/src/client/image-component.tsx index 37563b970af75..1e635c8293547 100644 --- a/packages/next/src/client/image-component.tsx +++ b/packages/next/src/client/image-component.tsx @@ -364,10 +364,24 @@ export const Image = forwardRef( const configContext = useContext(ImageConfigContext) const config = useMemo(() => { const c = configEnv || configContext || imageConfigDefault + const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b) const deviceSizes = c.deviceSizes.sort((a, b) => a - b) const qualities = c.qualities?.sort((a, b) => a - b) - return { ...c, allSizes, deviceSizes, qualities } + return { + ...c, + allSizes, + deviceSizes, + qualities, + // During the SSR, configEnv (__NEXT_IMAGE_OPTS) does not include + // security sensitive configs like `localPatterns`, which is needed + // during the server render to ensure it's validated. Therefore use + // configContext, which holds the config from the server for validation. + localPatterns: + typeof window === 'undefined' + ? configContext?.localPatterns + : c.localPatterns, + } }, [configContext]) const { onLoad, onLoadingComplete } = props @@ -385,7 +399,6 @@ export const Image = forwardRef( const [blurComplete, setBlurComplete] = useState(false) const [showAltText, setShowAltText] = useState(false) - const { props: imgAttributes, meta: imgMeta } = getImgProps(props, { defaultLoader, imgConf: config, diff --git a/packages/next/src/client/legacy/image.tsx b/packages/next/src/client/legacy/image.tsx index 3e2a34d47062c..ffd8ab57384aa 100644 --- a/packages/next/src/client/legacy/image.tsx +++ b/packages/next/src/client/legacy/image.tsx @@ -120,6 +120,25 @@ function defaultLoader({ width, quality, }: ImageLoaderPropsWithConfig): string { + if (!config.dangerouslyAllowSVG && src.split('?', 1)[0].endsWith('.svg')) { + // Special case to make svg serve as-is to avoid proxying + // through the built-in Image Optimization API. + return src + } + + if ( + src.startsWith('/') && + src.includes('?') && + config.localPatterns?.length === 1 && + config.localPatterns[0].pathname === '**' && + config.localPatterns[0].search === '' + ) { + throw new Error( + `Image with src "${src}" is using a query string which is not configured in images.localPatterns.` + + `\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns` + ) + } + if (process.env.NODE_ENV !== 'production') { const missingValues = [] @@ -192,12 +211,6 @@ function defaultLoader({ const q = findClosestQuality(quality, config) - if (!config.dangerouslyAllowSVG && src.split('?', 1)[0].endsWith('.svg')) { - // Special case to make svg serve as-is to avoid proxying - // through the built-in Image Optimization API. - return src - } - return `${normalizePathTrailingSlash(config.path)}?url=${encodeURIComponent( src )}&w=${width}&q=${q}` @@ -645,7 +658,19 @@ export default function Image({ const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b) const deviceSizes = c.deviceSizes.sort((a, b) => a - b) const qualities = c.qualities?.sort((a, b) => a - b) - return { ...c, allSizes, deviceSizes, qualities } + return { + ...c, + allSizes, + deviceSizes, + qualities, // During the SSR, configEnv (__NEXT_IMAGE_OPTS) does not include + // security sensitive configs like `localPatterns`, which is needed + // during the server render to ensure it's validated. Therefore use + // configContext, which holds the config from the server for validation. + localPatterns: + typeof window === 'undefined' + ? configContext?.localPatterns + : c.localPatterns, + } }, [configContext]) let rest: Partial = all @@ -865,19 +890,6 @@ export default function Image({ ) } - if ( - src.startsWith('/') && - src.includes('?') && - (!config?.localPatterns?.length || - (config.localPatterns.length === 1 && - config.localPatterns[0].pathname === '/_next/static/media/**')) - ) { - warnOnce( - `Image with src "${src}" is using a query string which is not configured in images.localPatterns. This config will be required starting in Next.js 16.` + - `\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns` - ) - } - if (!unoptimized && loader !== defaultImageLoader) { const urlStr = loader({ config, diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 4ea3849578dd4..7ace45d66e494 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -209,6 +209,8 @@ import { import type { ExperimentalConfig } from '../config-shared' import type { Params } from '../request/params' import { createPromiseWithResolvers } from '../../shared/lib/promise-with-resolvers' +import { ImageConfigContext } from '../../shared/lib/image-config-context.shared-runtime' +import { imageConfigDefault } from '../../shared/lib/image-config' export type GetDynamicParamFromSegment = ( // [slug] / [[slug]] / [...slug] @@ -1359,12 +1361,14 @@ function App({ clientReferenceManifest, ServerInsertedHTMLProvider, nonce, + images, }: { reactServerStream: BinaryStreamOf reactDebugStream: ReadableStream | undefined preinitScripts: () => void clientReferenceManifest: NonNullable ServerInsertedHTMLProvider: React.ComponentType<{ children: JSX.Element }> + images: RenderOpts['images'] nonce?: string }): JSX.Element { preinitScripts() @@ -1404,9 +1408,11 @@ function App({ nonce, }} > - - - + + + + + ) } @@ -1421,6 +1427,7 @@ function ErrorApp({ clientReferenceManifest, ServerInsertedHTMLProvider, nonce, + images, }: { reactServerStream: BinaryStreamOf reactDebugStream: ReadableStream | undefined @@ -1428,6 +1435,7 @@ function ErrorApp({ clientReferenceManifest: NonNullable ServerInsertedHTMLProvider: React.ComponentType<{ children: JSX.Element }> nonce?: string + images: RenderOpts['images'] }): JSX.Element { preinitScripts() const response = React.use( @@ -1457,9 +1465,11 @@ function ErrorApp({ const actionQueue = createMutableActionQueue(initialState, null) return ( - - - + + + + + ) } @@ -2422,6 +2432,7 @@ async function renderToStream( clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={nonce} + images={ctx.renderOpts.images} />, postponed, { onError: htmlRendererErrorHandler, nonce } @@ -2467,6 +2478,7 @@ async function renderToStream( clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={nonce} + images={ctx.renderOpts.images} />, { onError: htmlRendererErrorHandler, @@ -2627,6 +2639,7 @@ async function renderToStream( preinitScripts={errorPreinitScripts} clientReferenceManifest={clientReferenceManifest} nonce={nonce} + images={ctx.renderOpts.images} /> ), streamOptions: { @@ -3010,6 +3023,7 @@ async function spawnDynamicValidationInDev( clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={nonce} + images={ctx.renderOpts.images} />, { signal: initialClientReactController.signal, @@ -3239,6 +3253,7 @@ async function spawnDynamicValidationInDev( clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={nonce} + images={ctx.renderOpts.images} />, { signal: finalClientReactController.signal, @@ -3753,6 +3768,7 @@ async function prerenderToStream( clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={nonce} + images={ctx.renderOpts.images} />, { signal: initialClientReactController.signal, @@ -3987,6 +4003,7 @@ async function prerenderToStream( clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={nonce} + images={ctx.renderOpts.images} />, { signal: finalClientReactController.signal, @@ -4151,6 +4168,7 @@ async function prerenderToStream( clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={nonce} + images={ctx.renderOpts.images} />, JSON.parse(JSON.stringify(postponed)), { @@ -4258,6 +4276,7 @@ async function prerenderToStream( clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={nonce} + images={ctx.renderOpts.images} />, { onError: htmlRendererErrorHandler, @@ -4399,6 +4418,7 @@ async function prerenderToStream( clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={nonce} + images={ctx.renderOpts.images} />, JSON.parse(JSON.stringify(postponed)), { @@ -4483,6 +4503,7 @@ async function prerenderToStream( clientReferenceManifest={clientReferenceManifest} ServerInsertedHTMLProvider={ServerInsertedHTMLProvider} nonce={nonce} + images={ctx.renderOpts.images} />, { onError: htmlRendererErrorHandler, @@ -4657,6 +4678,7 @@ async function prerenderToStream( preinitScripts={errorPreinitScripts} clientReferenceManifest={clientReferenceManifest} nonce={nonce} + images={ctx.renderOpts.images} /> ), streamOptions: { diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index 217760cb650c3..55298d6702d47 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -9,6 +9,7 @@ import type { NextFontManifest } from '../../build/webpack/plugins/next-font-man import type { ParsedUrlQuery } from 'querystring' import type { AppPageModule } from '../route-modules/app-page/module' import type { DeepReadonly } from '../../shared/lib/deep-readonly' +import type { ImageConfigComplete } from '../../shared/lib/image-config' import type { __ApiPreviewProps } from '../api-utils' import s from 'next/dist/compiled/superstruct' @@ -77,6 +78,7 @@ export interface RenderOptsPartial { dev?: boolean basePath: string trailingSlash: boolean + images: ImageConfigComplete clientReferenceManifest?: DeepReadonly supportsDynamicResponse: boolean runtime?: ServerRuntime diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 8ec4908692d13..53d7cebe493e0 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -477,6 +477,14 @@ function assignDefaultsAndValidate( search: '', }) } + } else { + // All paths are not allowed for a search query by default. + images.localPatterns = [ + { + pathname: '**', + search: '', + }, + ] } if (images.remotePatterns) { diff --git a/packages/next/src/shared/lib/get-img-props.ts b/packages/next/src/shared/lib/get-img-props.ts index 5fd6cf5825bed..50073562aa20b 100644 --- a/packages/next/src/shared/lib/get-img-props.ts +++ b/packages/next/src/shared/lib/get-img-props.ts @@ -560,18 +560,6 @@ export function getImgProps( `\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-qualities` ) } - if ( - src.startsWith('/') && - src.includes('?') && - (!config?.localPatterns?.length || - (config.localPatterns.length === 1 && - config.localPatterns[0].pathname === '/_next/static/media/**')) - ) { - warnOnce( - `Image with src "${src}" is using a query string which is not configured in images.localPatterns. This config will be required starting in Next.js 16.` + - `\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns` - ) - } if (placeholder === 'blur' && !blurDataURL) { const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next-image-loader diff --git a/packages/next/src/shared/lib/image-loader.ts b/packages/next/src/shared/lib/image-loader.ts index 5eaa61217b8ab..8be9d54d8f1cb 100644 --- a/packages/next/src/shared/lib/image-loader.ts +++ b/packages/next/src/shared/lib/image-loader.ts @@ -7,6 +7,19 @@ function defaultLoader({ width, quality, }: ImageLoaderPropsWithConfig): string { + if ( + src.startsWith('/') && + src.includes('?') && + config.localPatterns?.length === 1 && + config.localPatterns[0].pathname === '**' && + config.localPatterns[0].search === '' + ) { + throw new Error( + `Image with src "${src}" is using a query string which is not configured in images.localPatterns.` + + `\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns` + ) + } + if (process.env.NODE_ENV !== 'production') { const missingValues = [] diff --git a/test/unit/next-image-get-img-props.test.ts b/test/unit/next-image-get-img-props.test.ts index dec2cb916bd3d..3900b588e5eca 100644 --- a/test/unit/next-image-get-img-props.test.ts +++ b/test/unit/next-image-get-img-props.test.ts @@ -574,26 +574,6 @@ describe('getImageProps()', () => { ['src', '/test.svg'], ]) }) - it('should auto unoptimized for relative svg with query', async () => { - const { props } = getImageProps({ - alt: 'a nice desc', - src: '/test.svg?v=1', - width: 100, - height: 200, - }) - expect(warningMessages).toStrictEqual([ - 'Image with src "/test.svg?v=1" is using a query string which is not configured in images.localPatterns. This config will be required starting in Next.js 16.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns', - ]) - 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'], - ]) - }) it('should auto unoptimized for absolute svg', async () => { const { props } = getImageProps({ alt: 'a nice desc', From 02b0d5405a0f3c5f3a147e907eb7188df72dd404 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Wed, 1 Oct 2025 15:23:19 +0200 Subject: [PATCH 2/5] Update docs Co-authored-by: Steven --- docs/01-app/03-api-reference/02-components/image.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/01-app/03-api-reference/02-components/image.mdx b/docs/01-app/03-api-reference/02-components/image.mdx index 10947926eaf0c..3c4088671d45e 100644 --- a/docs/01-app/03-api-reference/02-components/image.mdx +++ b/docs/01-app/03-api-reference/02-components/image.mdx @@ -528,6 +528,8 @@ module.exports = { The example above will ensure the `src` property of `next/image` must start with `/assets/images/` and must not have a query string. Attempting to optimize any other path will respond with `400` Bad Request error. +> **Good to know**: Omitting the `search` property allows all search parameters which could allow malicious actors to optimize URLs you did not intend. Try using a specific value like `search: '?v=2'` to ensure an exact match. + #### `remotePatterns` Use `remotePatterns` in your `next.config.js` file to allow images from specific external paths and block all others. This ensures that only external images from your account can be served. From c9b2635e1d87cac98aff94075b86e90c5458ca2e Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Wed, 8 Oct 2025 16:12:04 +0200 Subject: [PATCH 3/5] Add tests --- .../app/layout.tsx | 8 +++++ .../app/page.tsx | 6 ++++ ...-with-query-without-local-patterns.test.ts | 28 ++++++++++++++++++ .../next.config.js | 6 ++++ .../public/test.png | Bin 0 -> 67 bytes .../app/layout.tsx | 8 +++++ .../app/page.tsx | 6 ++++ ...-with-query-without-local-patterns.test.ts | 28 ++++++++++++++++++ .../next.config.js | 6 ++++ .../public/test.png | Bin 0 -> 67 bytes ...-with-query-without-local-patterns.test.ts | 28 ++++++++++++++++++ .../next.config.js | 6 ++++ .../pages/index.tsx | 6 ++++ .../public/test.png | Bin 0 -> 67 bytes ...-with-query-without-local-patterns.test.ts | 28 ++++++++++++++++++ .../next.config.js | 6 ++++ .../pages/index.tsx | 6 ++++ .../public/test.png | Bin 0 -> 67 bytes 18 files changed, 176 insertions(+) create mode 100644 test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/layout.tsx create mode 100644 test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/page.tsx create mode 100644 test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts create mode 100644 test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next.config.js create mode 100644 test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/public/test.png create mode 100644 test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/layout.tsx create mode 100644 test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/page.tsx create mode 100644 test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts create mode 100644 test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next.config.js create mode 100644 test/e2e/app-dir/next-image-src-with-query-without-local-patterns/public/test.png create mode 100644 test/e2e/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts create mode 100644 test/e2e/next-image-legacy-src-with-query-without-local-patterns/next.config.js create mode 100644 test/e2e/next-image-legacy-src-with-query-without-local-patterns/pages/index.tsx create mode 100644 test/e2e/next-image-legacy-src-with-query-without-local-patterns/public/test.png create mode 100644 test/e2e/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts create mode 100644 test/e2e/next-image-src-with-query-without-local-patterns/next.config.js create mode 100644 test/e2e/next-image-src-with-query-without-local-patterns/pages/index.tsx create mode 100644 test/e2e/next-image-src-with-query-without-local-patterns/public/test.png diff --git a/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/layout.tsx b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/page.tsx b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/page.tsx new file mode 100644 index 0000000000000..ae502dc06f3b1 --- /dev/null +++ b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/app/page.tsx @@ -0,0 +1,6 @@ +import Image from 'next/legacy/image' + +export default function Page() { + // src with query without localPatterns will throw an error + return test +} diff --git a/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts new file mode 100644 index 0000000000000..c67f8f06261ac --- /dev/null +++ b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts @@ -0,0 +1,28 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('next-image-legacy-src-with-query-without-local-patterns', () => { + const { next, isNextDev, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + + if (skipped) { + return + } + + it('should throw error for relative image with query without localPatterns for legacy Image', async () => { + if (isNextDev) { + await next.start() + await next.browser('/') + expect(next.cliOutput).toContain( + 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns' + ) + } else { + const { cliOutput } = await next.build() + expect(cliOutput).toContain( + 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns' + ) + } + }) +}) diff --git a/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next.config.js b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/public/test.png b/test/e2e/app-dir/next-image-legacy-src-with-query-without-local-patterns/public/test.png new file mode 100644 index 0000000000000000000000000000000000000000..aea7f5ff8ad3e3c7c084080bda8ac3b95e9a71cd GIT binary patch literal 67 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`_bPE>9Q7kcv6U2|zXz1Ea_KC51p1 NgQu&X%Q~loCIDk>43q!> literal 0 HcmV?d00001 diff --git a/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/layout.tsx b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/page.tsx b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/page.tsx new file mode 100644 index 0000000000000..48b6eeeafd74f --- /dev/null +++ b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/app/page.tsx @@ -0,0 +1,6 @@ +import Image from 'next/image' + +export default function Page() { + // src with query without localPatterns will throw an error + return test +} diff --git a/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts new file mode 100644 index 0000000000000..4a577b1c5493a --- /dev/null +++ b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts @@ -0,0 +1,28 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('next-image-src-with-query-without-local-patterns', () => { + const { next, isNextDev, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + + if (skipped) { + return + } + + it('should throw error for relative image with query without localPatterns', async () => { + if (isNextDev) { + await next.start() + await next.browser('/') + expect(next.cliOutput).toContain( + 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns' + ) + } else { + const { cliOutput } = await next.build() + expect(cliOutput).toContain( + 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns' + ) + } + }) +}) diff --git a/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next.config.js b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/public/test.png b/test/e2e/app-dir/next-image-src-with-query-without-local-patterns/public/test.png new file mode 100644 index 0000000000000000000000000000000000000000..aea7f5ff8ad3e3c7c084080bda8ac3b95e9a71cd GIT binary patch literal 67 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`_bPE>9Q7kcv6U2|zXz1Ea_KC51p1 NgQu&X%Q~loCIDk>43q!> literal 0 HcmV?d00001 diff --git a/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts new file mode 100644 index 0000000000000..c67f8f06261ac --- /dev/null +++ b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next-image-legacy-src-with-query-without-local-patterns.test.ts @@ -0,0 +1,28 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('next-image-legacy-src-with-query-without-local-patterns', () => { + const { next, isNextDev, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + + if (skipped) { + return + } + + it('should throw error for relative image with query without localPatterns for legacy Image', async () => { + if (isNextDev) { + await next.start() + await next.browser('/') + expect(next.cliOutput).toContain( + 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns' + ) + } else { + const { cliOutput } = await next.build() + expect(cliOutput).toContain( + 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns' + ) + } + }) +}) diff --git a/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next.config.js b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/next-image-legacy-src-with-query-without-local-patterns/pages/index.tsx b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/pages/index.tsx new file mode 100644 index 0000000000000..ae502dc06f3b1 --- /dev/null +++ b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/pages/index.tsx @@ -0,0 +1,6 @@ +import Image from 'next/legacy/image' + +export default function Page() { + // src with query without localPatterns will throw an error + return test +} diff --git a/test/e2e/next-image-legacy-src-with-query-without-local-patterns/public/test.png b/test/e2e/next-image-legacy-src-with-query-without-local-patterns/public/test.png new file mode 100644 index 0000000000000000000000000000000000000000..aea7f5ff8ad3e3c7c084080bda8ac3b95e9a71cd GIT binary patch literal 67 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`_bPE>9Q7kcv6U2|zXz1Ea_KC51p1 NgQu&X%Q~loCIDk>43q!> literal 0 HcmV?d00001 diff --git a/test/e2e/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts b/test/e2e/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts new file mode 100644 index 0000000000000..4a577b1c5493a --- /dev/null +++ b/test/e2e/next-image-src-with-query-without-local-patterns/next-image-src-with-query-without-local-patterns.test.ts @@ -0,0 +1,28 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('next-image-src-with-query-without-local-patterns', () => { + const { next, isNextDev, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + + if (skipped) { + return + } + + it('should throw error for relative image with query without localPatterns', async () => { + if (isNextDev) { + await next.start() + await next.browser('/') + expect(next.cliOutput).toContain( + 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns' + ) + } else { + const { cliOutput } = await next.build() + expect(cliOutput).toContain( + 'Image with src "/test.png?v=1" is using a query string which is not configured in images.localPatterns.\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns' + ) + } + }) +}) diff --git a/test/e2e/next-image-src-with-query-without-local-patterns/next.config.js b/test/e2e/next-image-src-with-query-without-local-patterns/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/next-image-src-with-query-without-local-patterns/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/next-image-src-with-query-without-local-patterns/pages/index.tsx b/test/e2e/next-image-src-with-query-without-local-patterns/pages/index.tsx new file mode 100644 index 0000000000000..48b6eeeafd74f --- /dev/null +++ b/test/e2e/next-image-src-with-query-without-local-patterns/pages/index.tsx @@ -0,0 +1,6 @@ +import Image from 'next/image' + +export default function Page() { + // src with query without localPatterns will throw an error + return test +} diff --git a/test/e2e/next-image-src-with-query-without-local-patterns/public/test.png b/test/e2e/next-image-src-with-query-without-local-patterns/public/test.png new file mode 100644 index 0000000000000000000000000000000000000000..aea7f5ff8ad3e3c7c084080bda8ac3b95e9a71cd GIT binary patch literal 67 zcmeAS@N?(olHy`uVBq!ia0vp^j3CSbBp9sfW`_bPE>9Q7kcv6U2|zXz1Ea_KC51p1 NgQu&X%Q~loCIDk>43q!> literal 0 HcmV?d00001 From b3a9185f14347d63443b266aa6496fec3b32a73d Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Wed, 8 Oct 2025 16:16:12 +0200 Subject: [PATCH 4/5] Update errors.json --- packages/next/errors.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/next/errors.json b/packages/next/errors.json index b92f4f03075a7..a24f4959aa194 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -868,5 +868,6 @@ "867": "The %s \"%s\" must export a %s or a \\`default\\` function", "868": "No reference found for param: %s in reference: %s", "869": "No reference found for segment: %s with reference: %s", - "870": "refresh can only be called from within a Server Action. See more info here: https://nextjs.org/docs/app/api-reference/functions/refresh" + "870": "refresh can only be called from within a Server Action. See more info here: https://nextjs.org/docs/app/api-reference/functions/refresh", + "871": "Image with src \"%s\" is using a query string which is not configured in images.localPatterns.\\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns" } From c72cf5046cd8b4c21fc31a64fa14a80d072c7118 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Thu, 9 Oct 2025 19:45:42 +0200 Subject: [PATCH 5/5] Update tests --- .../next-image-new/app-dir-qualities/test/index.test.ts | 8 +++++++- .../integration/next-image-new/app-dir/test/index.test.ts | 8 +++++++- .../integration/next-image-new/unicode/test/index.test.ts | 7 +++++++ .../next-image-new/unoptimized/test/index.test.ts | 8 +++++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/test/integration/next-image-new/app-dir-qualities/test/index.test.ts b/test/integration/next-image-new/app-dir-qualities/test/index.test.ts index 8ff2a1da9a331..67efd5f01a011 100644 --- a/test/integration/next-image-new/app-dir-qualities/test/index.test.ts +++ b/test/integration/next-image-new/app-dir-qualities/test/index.test.ts @@ -101,7 +101,13 @@ function runTests(mode: 'dev' | 'server') { loader: 'default', loaderFile: '', remotePatterns: [], - localPatterns: undefined, + localPatterns: [ + { + pathname: + '^(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$))(?:(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$)).)*?)\\/?)$', + search: '', + }, + ], minimumCacheTTL: 14400, path: '/_next/image', qualities: [42, 69, 88], diff --git a/test/integration/next-image-new/app-dir/test/index.test.ts b/test/integration/next-image-new/app-dir/test/index.test.ts index b3d3d9f94042d..c316d738c5b98 100644 --- a/test/integration/next-image-new/app-dir/test/index.test.ts +++ b/test/integration/next-image-new/app-dir/test/index.test.ts @@ -1787,7 +1787,13 @@ function runTests(mode: 'dev' | 'server') { loader: 'default', loaderFile: '', remotePatterns: [], - localPatterns: undefined, + localPatterns: [ + { + pathname: + '^(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$))(?:(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$)).)*?)\\/?)$', + search: '', + }, + ], minimumCacheTTL: 14400, path: '/_next/image', qualities: [75], diff --git a/test/integration/next-image-new/unicode/test/index.test.ts b/test/integration/next-image-new/unicode/test/index.test.ts index e7682fc0d5003..34c6996e43503 100644 --- a/test/integration/next-image-new/unicode/test/index.test.ts +++ b/test/integration/next-image-new/unicode/test/index.test.ts @@ -94,6 +94,13 @@ function runTests(mode: 'server' | 'dev') { search: '', }, ], + localPatterns: [ + { + pathname: + '^(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$))(?:(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$)).)*?)\\/?)$', + search: '', + }, + ], minimumCacheTTL: 14400, path: '/_next/image', qualities: [75], diff --git a/test/integration/next-image-new/unoptimized/test/index.test.ts b/test/integration/next-image-new/unoptimized/test/index.test.ts index 630278c1a8e9b..2304d3e784cd4 100644 --- a/test/integration/next-image-new/unoptimized/test/index.test.ts +++ b/test/integration/next-image-new/unoptimized/test/index.test.ts @@ -109,7 +109,13 @@ function runTests(url: string, mode: 'dev' | 'server') { loader: 'default', loaderFile: '', remotePatterns: [], - localPatterns: undefined, + localPatterns: [ + { + pathname: + '^(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$))(?:(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$)).)*?)\\/?)$', + search: '', + }, + ], minimumCacheTTL: 14400, path: '/_next/image', qualities: [75],