From 57d2963507f4c166b788347eba6848892ff922fd Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 22 Feb 2023 14:56:51 -0500 Subject: [PATCH] feat: add `contentDispositionType` config to Image Optimization API (#46254) Add `contentDispositionType` config to Image Optimization API so the user can configure `inline` vs `attachment`. This is recommended when `dangerouslyAllowSVG` is enabled but can also be used when its disabled. --- docs/api-reference/next/image.md | 6 +- docs/api-reference/next/legacy/image.md | 5 +- errors/invalid-images-config.md | 2 + packages/next/src/server/config-schema.ts | 4 + packages/next/src/server/image-optimizer.ts | 25 +++-- packages/next/src/server/next-server.ts | 2 +- packages/next/src/shared/lib/image-config.ts | 4 + .../test/content-disposition-type.test.ts | 13 +++ test/integration/image-optimizer/test/util.ts | 99 ++++++++++--------- 9 files changed, 99 insertions(+), 61 deletions(-) create mode 100644 test/integration/image-optimizer/test/content-disposition-type.test.ts diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 3174c42aced7b..f9418a3eea9b7 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -16,6 +16,7 @@ description: Enable Image Optimization with the built-in Image component. | Version | Changes | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `v13.2.0` | `contentDispositionType` configuration added. | | `v13.0.6` | `ref` prop added. | | `v13.0.0` | `` wrapper removed. `layout`, `objectFit`, `objectPosition`, `lazyBoundary`, `lazyRoot` props removed. `alt` is required. `onLoadingComplete` receives reference to `img` element. Built-in loader config removed. | | `v12.3.0` | `remotePatterns` and `unoptimized` configuration is stable. | @@ -503,17 +504,20 @@ module.exports = { The default [loader](#loader) does not optimize SVG images for a few reasons. First, SVG is a vector format meaning it can be resized losslessly. Second, SVG has many of the same features as HTML/CSS, which can lead to vulnerabilities without proper [Content Security Policy (CSP) headers](/docs/advanced-features/security-headers.md). -If you need to serve SVG images with the default Image Optimization API, you can set `dangerouslyAllowSVG` and `contentSecurityPolicy` inside your `next.config.js`: +If you need to serve SVG images with the default Image Optimization API, you can set `dangerouslyAllowSVG` inside your `next.config.js`: ```js module.exports = { images: { dangerouslyAllowSVG: true, + contentDispositionType: 'attachment', contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", }, } ``` +In addition, it is strongly recommended to also set `contentDispositionType` to force the browser to download the image, as well as `contentSecurityPolicy` to prevent scripts embedded in the image from executing. + ### Animated Images The default [loader](#loader) will automatically bypass Image Optimization for animated images and serve the image as-is. diff --git a/docs/api-reference/next/legacy/image.md b/docs/api-reference/next/legacy/image.md index c6b8176c4d169..f5ef5d2468b13 100644 --- a/docs/api-reference/next/legacy/image.md +++ b/docs/api-reference/next/legacy/image.md @@ -571,17 +571,20 @@ module.exports = { The default [loader](#loader) does not optimize SVG images for a few reasons. First, SVG is a vector format meaning it can be resized losslessly. Second, SVG has many of the same features as HTML/CSS, which can lead to vulnerabilities without proper [Content Security Policy (CSP) headers](/docs/advanced-features/security-headers.md). -If you need to serve SVG images with the default Image Optimization API, you can set `dangerouslyAllowSVG` and `contentSecurityPolicy` inside your `next.config.js`: +If you need to serve SVG images with the default Image Optimization API, you can set `dangerouslyAllowSVG` inside your `next.config.js`: ```js module.exports = { images: { dangerouslyAllowSVG: true, + contentDispositionType: 'attachment', contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", }, } ``` +In addition, it is strongly recommended to also set `contentDispositionType` to force the browser to download the image, as well as `contentSecurityPolicy` to prevent scripts embedded in the image from executing. + ### Animated Images The default [loader](#loader) will automatically bypass Image Optimization for animated images and serve the image as-is. diff --git a/errors/invalid-images-config.md b/errors/invalid-images-config.md index badaa4f3e1a88..d76741e61f3d9 100644 --- a/errors/invalid-images-config.md +++ b/errors/invalid-images-config.md @@ -33,6 +33,8 @@ module.exports = { dangerouslyAllowSVG: false, // set the Content-Security-Policy header contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", + // sets the Content-Disposition header (inline or attachment) + contentDispositionType: 'inline', // limit of 50 objects remotePatterns: [], // when true, every image will be unoptimized diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 10f6280dd57ff..d4b262fc9bc27 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -598,6 +598,10 @@ const configSchema = { minLength: 1, type: 'string', }, + contentDispositionType: { + enum: ['inline', 'attachment'] as any, // automatic typing does not like enum + type: 'string', + }, dangerouslyAllowSVG: { type: 'boolean', }, diff --git a/packages/next/src/server/image-optimizer.ts b/packages/next/src/server/image-optimizer.ts index 6f881fbb7eeef..f0fa31bd6d579 100644 --- a/packages/next/src/server/image-optimizer.ts +++ b/packages/next/src/server/image-optimizer.ts @@ -23,6 +23,7 @@ import { IncrementalCacheEntry, IncrementalCacheValue } from './response-cache' import { mockRequest } from './lib/mock-request' import { hasMatch } from '../shared/lib/match-remote-pattern' import { getImageBlurSvg } from '../shared/lib/image-blur-svg' +import { ImageConfigComplete } from '../shared/lib/image-config' type XCacheHeader = 'MISS' | 'HIT' | 'STALE' @@ -672,11 +673,11 @@ export async function imageOptimizer( function getFileNameWithExtension( url: string, contentType: string | null -): string | void { +): string { const [urlWithoutQueryParams] = url.split('?') const fileNameWithExtension = urlWithoutQueryParams.split('/').pop() if (!contentType || !fileNameWithExtension) { - return + return 'image.bin' } const [fileName] = fileNameWithExtension.split('.') @@ -692,7 +693,7 @@ function setResponseHeaders( contentType: string | null, isStatic: boolean, xCache: XCacheHeader, - contentSecurityPolicy: string, + imagesConfig: ImageConfigComplete, maxAge: number, isDev: boolean ) { @@ -712,16 +713,12 @@ function setResponseHeaders( } const fileName = getFileNameWithExtension(url, contentType) - if (fileName) { - res.setHeader( - 'Content-Disposition', - contentDisposition(fileName, { type: 'inline' }) - ) - } + res.setHeader( + 'Content-Disposition', + contentDisposition(fileName, { type: imagesConfig.contentDispositionType }) + ) - if (contentSecurityPolicy) { - res.setHeader('Content-Security-Policy', contentSecurityPolicy) - } + res.setHeader('Content-Security-Policy', imagesConfig.contentSecurityPolicy) res.setHeader('X-Nextjs-Cache', xCache) return { finished: false } @@ -735,7 +732,7 @@ export function sendResponse( buffer: Buffer, isStatic: boolean, xCache: XCacheHeader, - contentSecurityPolicy: string, + imagesConfig: ImageConfigComplete, maxAge: number, isDev: boolean ) { @@ -749,7 +746,7 @@ export function sendResponse( contentType, isStatic, xCache, - contentSecurityPolicy, + imagesConfig, maxAge, isDev ) diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 6f5dec54a3f18..34808e1ceb7f5 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -466,7 +466,7 @@ export default class NextNodeServer extends BaseServer { cacheEntry.value.buffer, paramsResult.isStatic, cacheEntry.isMiss ? 'MISS' : cacheEntry.isStale ? 'STALE' : 'HIT', - imagesConfig.contentSecurityPolicy, + imagesConfig, cacheEntry.revalidate || 0, Boolean(this.renderOpts.dev) ) diff --git a/packages/next/src/shared/lib/image-config.ts b/packages/next/src/shared/lib/image-config.ts index e13cdb6943712..34566883ab647 100644 --- a/packages/next/src/shared/lib/image-config.ts +++ b/packages/next/src/shared/lib/image-config.ts @@ -88,6 +88,9 @@ export type ImageConfigComplete = { /** @see [Dangerously Allow SVG](https://nextjs.org/docs/api-reference/next/image#dangerously-allow-svg) */ contentSecurityPolicy: string + /** @see [Dangerously Allow SVG](https://nextjs.org/docs/api-reference/next/image#dangerously-allow-svg) */ + contentDispositionType: 'inline' | 'attachment' + /** @see [Remote Patterns](https://nextjs.org/docs/api-reference/next/image#remote-patterns) */ remotePatterns: RemotePattern[] @@ -109,6 +112,7 @@ export const imageConfigDefault: ImageConfigComplete = { formats: ['image/webp'], dangerouslyAllowSVG: false, contentSecurityPolicy: `script-src 'none'; frame-src 'none'; sandbox;`, + contentDispositionType: 'inline', remotePatterns: [], unoptimized: false, } diff --git a/test/integration/image-optimizer/test/content-disposition-type.test.ts b/test/integration/image-optimizer/test/content-disposition-type.test.ts new file mode 100644 index 0000000000000..606958b11a1ce --- /dev/null +++ b/test/integration/image-optimizer/test/content-disposition-type.test.ts @@ -0,0 +1,13 @@ +import { join } from 'path' +import { setupTests } from './util' + +const appDir = join(__dirname, '../app') +const imagesDir = join(appDir, '.next', 'cache', 'images') + +describe('with contentDispositionType attachment', () => { + setupTests({ + nextConfigImages: { contentDispositionType: 'attachment' }, + appDir, + imagesDir, + }) +}) diff --git a/test/integration/image-optimizer/test/util.ts b/test/integration/image-optimizer/test/util.ts index 065ea5410354f..4f75ab159c7fa 100644 --- a/test/integration/image-optimizer/test/util.ts +++ b/test/integration/image-optimizer/test/util.ts @@ -131,7 +131,8 @@ async function fetchWithDuration( } export function runTests(ctx) { - const { isDev, minimumCacheTTL = 60 } = ctx + const { isDev, minimumCacheTTL = 60, nextConfigImages } = ctx + const { contentDispositionType = 'inline' } = nextConfigImages || {} let slowImageServer: Awaited> beforeAll(async () => { slowImageServer = await serveSlowImage() @@ -178,7 +179,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="animated.gif"` + `${contentDispositionType}; filename="animated.gif"` ) await expectWidth(res, 50, { expectAnimated: true }) }) @@ -194,7 +195,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="animated.png"` + `${contentDispositionType}; filename="animated.png"` ) await expectWidth(res, 100, { expectAnimated: true }) }) @@ -210,7 +211,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="animated2.png"` + `${contentDispositionType}; filename="animated2.png"` ) await expectWidth(res, 1105, { expectAnimated: true }) }) @@ -226,7 +227,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="animated.webp"` + `${contentDispositionType}; filename="animated.webp"` ) await expectWidth(res, 400, { expectAnimated: true }) }) @@ -247,7 +248,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toMatch(/^Accept(,|$)/) expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.svg"` + `${contentDispositionType}; filename="test.svg"` ) const actual = await res.text() const expected = await fs.readFile( @@ -308,7 +309,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toMatch(/^Accept(,|$)/) expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.ico"` + `${contentDispositionType}; filename="test.ico"` ) const actual = await res.text() const expected = await fs.readFile( @@ -332,7 +333,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.jpeg"` + `${contentDispositionType}; filename="test.jpeg"` ) }) @@ -350,7 +351,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.png"` + `${contentDispositionType}; filename="test.png"` ) }) @@ -368,7 +369,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.jpeg"` + `${contentDispositionType}; filename="test.jpeg"` ) await expectWidth(res, ctx.w) }) @@ -388,7 +389,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.jpeg"` + `${contentDispositionType}; filename="test.jpeg"` ) await expectWidth(res, ctx.w) }) @@ -532,7 +533,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.webp"` + `${contentDispositionType}; filename="test.webp"` ) await expectWidth(res, ctx.w) }) @@ -549,7 +550,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.png"` + `${contentDispositionType}; filename="test.png"` ) await expectWidth(res, ctx.w) }) @@ -566,7 +567,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.png"` + `${contentDispositionType}; filename="test.png"` ) await expectWidth(res, ctx.w) }) @@ -583,7 +584,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.gif"` + `${contentDispositionType}; filename="test.gif"` ) // FIXME: await expectWidth(res, ctx.w) }) @@ -600,7 +601,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.tiff"` + `${contentDispositionType}; filename="test.tiff"` ) // FIXME: await expectWidth(res, ctx.w) }) @@ -619,7 +620,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.webp"` + `${contentDispositionType}; filename="test.webp"` ) await expectWidth(res, ctx.w) }) @@ -641,7 +642,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.avif"` + `${contentDispositionType}; filename="test.avif"` ) // TODO: upgrade "image-size" package to support AVIF // See https://github.com/image-size/image-size/issues/348 @@ -675,7 +676,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.webp"` + `${contentDispositionType}; filename="test.webp"` ) await expectWidth(res, ctx.w) }) @@ -698,7 +699,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="png-as-octet-stream.webp"` + `${contentDispositionType}; filename="png-as-octet-stream.webp"` ) await expectWidth(res, ctx.w) }) @@ -722,7 +723,7 @@ export function runTests(ctx) { expect(one.res.headers.get('X-Nextjs-Cache')).toBe('MISS') expect(one.res.headers.get('Content-Type')).toBe('image/webp') expect(one.res.headers.get('Content-Disposition')).toBe( - `inline; filename="slow.webp"` + `${contentDispositionType}; filename="slow.webp"` ) const etagOne = one.res.headers.get('etag') @@ -746,7 +747,7 @@ export function runTests(ctx) { expect(two.res.headers.get('X-Nextjs-Cache')).toBe('HIT') expect(two.res.headers.get('Content-Type')).toBe('image/webp') expect(two.res.headers.get('Content-Disposition')).toBe( - `inline; filename="slow.webp"` + `${contentDispositionType}; filename="slow.webp"` ) const json2 = await fsToJson(ctx.imagesDir) expect(json2).toStrictEqual(json1) @@ -765,7 +766,7 @@ export function runTests(ctx) { expect(three.res.headers.get('X-Nextjs-Cache')).toBe('STALE') expect(three.res.headers.get('Content-Type')).toBe('image/webp') expect(three.res.headers.get('Content-Disposition')).toBe( - `inline; filename="slow.webp"` + `${contentDispositionType}; filename="slow.webp"` ) expect(four.duration).toBeLessThan(one.duration) @@ -773,7 +774,7 @@ export function runTests(ctx) { expect(four.res.headers.get('X-Nextjs-Cache')).toBe('STALE') expect(four.res.headers.get('Content-Type')).toBe('image/webp') expect(four.res.headers.get('Content-Disposition')).toBe( - `inline; filename="slow.webp"` + `${contentDispositionType}; filename="slow.webp"` ) await check(async () => { const json4 = await fsToJson(ctx.imagesDir) @@ -796,7 +797,7 @@ export function runTests(ctx) { expect(five.res.headers.get('X-Nextjs-Cache')).toBe('HIT') expect(five.res.headers.get('Content-Type')).toBe('image/webp') expect(five.res.headers.get('Content-Disposition')).toBe( - `inline; filename="slow.webp"` + `${contentDispositionType}; filename="slow.webp"` ) await check(async () => { const json5 = await fsToJson(ctx.imagesDir) @@ -868,7 +869,7 @@ export function runTests(ctx) { expect(one.res.headers.get('X-Nextjs-Cache')).toBe('MISS') expect(one.res.headers.get('Content-Type')).toBe('image/webp') expect(one.res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.webp"` + `${contentDispositionType}; filename="test.webp"` ) const etagOne = one.res.headers.get('etag') @@ -892,7 +893,7 @@ export function runTests(ctx) { expect(two.res.headers.get('X-Nextjs-Cache')).toBe('HIT') expect(two.res.headers.get('Content-Type')).toBe('image/webp') expect(two.res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.webp"` + `${contentDispositionType}; filename="test.webp"` ) const json2 = await fsToJson(ctx.imagesDir) expect(json2).toStrictEqual(json1) @@ -911,7 +912,7 @@ export function runTests(ctx) { expect(three.res.headers.get('X-Nextjs-Cache')).toBe('STALE') expect(three.res.headers.get('Content-Type')).toBe('image/webp') expect(three.res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.webp"` + `${contentDispositionType}; filename="test.webp"` ) expect(four.duration).toBeLessThan(one.duration) @@ -919,7 +920,7 @@ export function runTests(ctx) { expect(four.res.headers.get('X-Nextjs-Cache')).toBe('STALE') expect(four.res.headers.get('Content-Type')).toBe('image/webp') expect(four.res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.webp"` + `${contentDispositionType}; filename="test.webp"` ) await check(async () => { const json3 = await fsToJson(ctx.imagesDir) @@ -942,7 +943,7 @@ export function runTests(ctx) { expect(five.res.headers.get('X-Nextjs-Cache')).toBe('HIT') expect(five.res.headers.get('Content-Type')).toBe('image/webp') expect(five.res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.webp"` + `${contentDispositionType}; filename="test.webp"` ) await check(async () => { const json5 = await fsToJson(ctx.imagesDir) @@ -968,7 +969,7 @@ export function runTests(ctx) { expect(res1.headers.get('X-Nextjs-Cache')).toBe('MISS') expect(res1.headers.get('Content-Type')).toBe('image/svg+xml') expect(res1.headers.get('Content-Disposition')).toBe( - `inline; filename="test.svg"` + `${contentDispositionType}; filename="test.svg"` ) const etagOne = res1.headers.get('etag') @@ -987,7 +988,7 @@ export function runTests(ctx) { expect(res2.headers.get('X-Nextjs-Cache')).toBe('HIT') expect(res2.headers.get('Content-Type')).toBe('image/svg+xml') expect(res2.headers.get('Content-Disposition')).toBe( - `inline; filename="test.svg"` + `${contentDispositionType}; filename="test.svg"` ) const json2 = await fsToJson(ctx.imagesDir) expect(json2).toStrictEqual(json1) @@ -1005,7 +1006,7 @@ export function runTests(ctx) { expect(res1.headers.get('X-Nextjs-Cache')).toBe('MISS') expect(res1.headers.get('Content-Type')).toBe('image/gif') expect(res1.headers.get('Content-Disposition')).toBe( - `inline; filename="animated.gif"` + `${contentDispositionType}; filename="animated.gif"` ) let json1 @@ -1019,7 +1020,7 @@ export function runTests(ctx) { expect(res2.headers.get('X-Nextjs-Cache')).toBe('HIT') expect(res2.headers.get('Content-Type')).toBe('image/gif') expect(res2.headers.get('Content-Disposition')).toBe( - `inline; filename="animated.gif"` + `${contentDispositionType}; filename="animated.gif"` ) const json2 = await fsToJson(ctx.imagesDir) expect(json2).toStrictEqual(json1) @@ -1039,7 +1040,7 @@ export function runTests(ctx) { const etag = res1.headers.get('Etag') expect(etag).toBeTruthy() expect(res1.headers.get('Content-Disposition')).toBe( - `inline; filename="test.webp"` + `${contentDispositionType}; filename="test.webp"` ) await expectWidth(res1, ctx.w) @@ -1066,7 +1067,7 @@ export function runTests(ctx) { expect(res3.headers.get('Etag')).toBeTruthy() expect(res3.headers.get('Etag')).not.toBe(etag) expect(res3.headers.get('Content-Disposition')).toBe( - `inline; filename="test.webp"` + `${contentDispositionType}; filename="test.webp"` ) await expectWidth(res3, ctx.w) }) @@ -1088,7 +1089,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toMatch(/^Accept(,|$)/) expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.bmp"` + `${contentDispositionType}; filename="test.bmp"` ) await check(async () => { @@ -1113,7 +1114,7 @@ export function runTests(ctx) { expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('etag')).toBeTruthy() expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="test.webp"` + `${contentDispositionType}; filename="test.webp"` ) await expectWidth(res, 400) }) @@ -1134,7 +1135,7 @@ export function runTests(ctx) { ) expect(res.headers.get('Vary')).toBe('Accept') expect(res.headers.get('Content-Disposition')).toBe( - `inline; filename="grayscale.png"` + `${contentDispositionType}; filename="grayscale.png"` ) const png = await res.buffer() @@ -1163,7 +1164,7 @@ export function runTests(ctx) { ) expect(res1.headers.get('Vary')).toBe('Accept') expect(res1.headers.get('Content-Disposition')).toBe( - `inline; filename="${filename}.webp"` + `${contentDispositionType}; filename="${filename}.webp"` ) await expectWidth(res1, ctx.w) @@ -1175,7 +1176,7 @@ export function runTests(ctx) { ) expect(res2.headers.get('Vary')).toBe('Accept') expect(res2.headers.get('Content-Disposition')).toBe( - `inline; filename="${filename}.webp"` + `${contentDispositionType}; filename="${filename}.webp"` ) await expectWidth(res2, ctx.w) } @@ -1223,15 +1224,15 @@ export function runTests(ctx) { expect(res1.headers.get('Content-Type')).toBe('image/webp') expect(res1.headers.get('Content-Disposition')).toBe( - `inline; filename="slow.webp"` + `${contentDispositionType}; filename="slow.webp"` ) expect(res2.headers.get('Content-Type')).toBe('image/webp') expect(res2.headers.get('Content-Disposition')).toBe( - `inline; filename="slow.webp"` + `${contentDispositionType}; filename="slow.webp"` ) expect(res3.headers.get('Content-Type')).toBe('image/webp') expect(res3.headers.get('Content-Disposition')).toBe( - `inline; filename="slow.webp"` + `${contentDispositionType}; filename="slow.webp"` ) await expectWidth(res1, ctx.w) @@ -1290,6 +1291,10 @@ export const setupTests = (ctx) => { // only run one server config with outdated sharp if (!ctx.isOutdatedSharp) { describe('dev support w/o next.config.js', () => { + if (ctx.nextConfigImages) { + // skip this test because it requires next.config.js + return + } const size = 384 // defaults defined in server/config.ts const curCtx = { ...ctx, @@ -1337,6 +1342,7 @@ export const setupTests = (ctx) => { imageSizes: [size], domains: curCtx.domains, formats: ['image/avif', 'image/webp'], + ...ctx.nextConfigImages, }, }) curCtx.nextOutput = '' @@ -1364,6 +1370,10 @@ export const setupTests = (ctx) => { }) describe('Server support w/o next.config.js', () => { + if (ctx.nextConfigImages) { + // skip this test because it requires next.config.js + return + } const size = 384 // defaults defined in server/config.ts const curCtx = { ...ctx, @@ -1410,6 +1420,7 @@ export const setupTests = (ctx) => { formats: ['image/avif', 'image/webp'], deviceSizes: [size, largeSize], domains: ctx.domains, + ...ctx.nextConfigImages, }, }) curCtx.nextOutput = ''