From 9a98ce553191494d1774b69f02d7bb3d0060305d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 06:14:59 +0000 Subject: [PATCH 1/3] cms-api: Add file name to Content-Disposition of DAM downloads The download endpoints sent a bare `attachment`, so browsers fell back to the URL for the name of the saved file. That URL only contains the file name without its extension, leaving downloads without one. Use `res.attachment()` instead of building the header by hand: file names are not slugified when a file is renamed, so they can contain quotes or non-latin1 characters, which need escaping and RFC 5987 encoding. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CxUhjh9AAnB6AHvGopkdCu --- .changeset/dam-download-content-disposition-filename.md | 7 +++++++ packages/api/cms-api/src/dam/files/files.controller.ts | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .changeset/dam-download-content-disposition-filename.md diff --git a/.changeset/dam-download-content-disposition-filename.md b/.changeset/dam-download-content-disposition-filename.md new file mode 100644 index 00000000000..3438c65c11b --- /dev/null +++ b/.changeset/dam-download-content-disposition-filename.md @@ -0,0 +1,7 @@ +--- +"@dextinity/cms-api": patch +--- + +Add the file name to the `Content-Disposition` header of DAM file downloads + +The header was previously set to `attachment` without a file name, so browsers derived the name of the downloaded file from the URL, which contains the file name without its extension. diff --git a/packages/api/cms-api/src/dam/files/files.controller.ts b/packages/api/cms-api/src/dam/files/files.controller.ts index d993e0dbeb5..71e46a50545 100644 --- a/packages/api/cms-api/src/dam/files/files.controller.ts +++ b/packages/api/cms-api/src/dam/files/files.controller.ts @@ -239,7 +239,7 @@ export function createFilesController({ Scope: PassedScope, damBasePath }: { Sco throw new ForbiddenException(); } - res.setHeader("Content-Disposition", "attachment"); + res.attachment(file.name); return this.streamFile(file, res, { range, overrideHeaders: { "cache-control": "max-age=31536000, private" } }); // Local caches only (1 year) } @@ -264,7 +264,7 @@ export function createFilesController({ Scope: PassedScope, damBasePath }: { Sco throw new BadRequestException("Content Hash mismatch!"); } - res.setHeader("Content-Disposition", "attachment"); + res.attachment(file.name); return this.streamFile(file, res, { range, overrideHeaders: { "cache-control": "max-age=31536000, s-maxage=86400, public" } }); // Public cache, 1 year for browsers, 1 day for proxies/cdn's } From 81225bd8cb55db6acb98bc917b5a300e4e534055 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 06:22:34 +0000 Subject: [PATCH 2/3] cms-api: Use content-disposition to build the DAM download header Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CxUhjh9AAnB6AHvGopkdCu --- packages/api/cms-api/package.json | 2 ++ packages/api/cms-api/src/dam/files/files.controller.ts | 5 +++-- pnpm-lock.yaml | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/api/cms-api/package.json b/packages/api/cms-api/package.json index 85393acedc7..2c57137c8f0 100644 --- a/packages/api/cms-api/package.json +++ b/packages/api/cms-api/package.json @@ -54,6 +54,7 @@ "@tiptap/pm": "^3.22.3", "@tiptap/starter-kit": "^3.22.3", "base64url": "^3.0.1", + "content-disposition": "^1.0.1", "cron-parser": "^3.5.0", "dataloader": "^2.2.3", "date-fns": "^4.4.0", @@ -99,6 +100,7 @@ "@sentry/node": "^9.47.1", "@smithy/types": "^4.14.3", "@swc/core": "^1.15.40", + "@types/content-disposition": "^0.5.9", "@types/draft-js": "^0.11.20", "@types/express": "^5.0.6", "@types/html-to-text": "^9.0.4", diff --git a/packages/api/cms-api/src/dam/files/files.controller.ts b/packages/api/cms-api/src/dam/files/files.controller.ts index 71e46a50545..a704b343fcf 100644 --- a/packages/api/cms-api/src/dam/files/files.controller.ts +++ b/packages/api/cms-api/src/dam/files/files.controller.ts @@ -18,6 +18,7 @@ import { } from "@nestjs/common"; import { plainToInstance } from "class-transformer"; import { validate } from "class-validator"; +import contentDisposition from "content-disposition"; import { Response } from "express"; import { OutgoingHttpHeaders } from "http"; import { basename, extname } from "path"; @@ -239,7 +240,7 @@ export function createFilesController({ Scope: PassedScope, damBasePath }: { Sco throw new ForbiddenException(); } - res.attachment(file.name); + res.setHeader("Content-Disposition", contentDisposition(file.name)); return this.streamFile(file, res, { range, overrideHeaders: { "cache-control": "max-age=31536000, private" } }); // Local caches only (1 year) } @@ -264,7 +265,7 @@ export function createFilesController({ Scope: PassedScope, damBasePath }: { Sco throw new BadRequestException("Content Hash mismatch!"); } - res.attachment(file.name); + res.setHeader("Content-Disposition", contentDisposition(file.name)); return this.streamFile(file, res, { range, overrideHeaders: { "cache-control": "max-age=31536000, s-maxage=86400, public" } }); // Public cache, 1 year for browsers, 1 day for proxies/cdn's } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb509d4cab7..b634ca5e265 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2077,6 +2077,9 @@ importers: base64url: specifier: ^3.0.1 version: 3.0.1 + content-disposition: + specifier: ^1.0.1 + version: 1.0.1 cron-parser: specifier: ^3.5.0 version: 3.5.0 @@ -2207,6 +2210,9 @@ importers: '@swc/core': specifier: ^1.15.40 version: 1.15.41 + '@types/content-disposition': + specifier: ^0.5.9 + version: 0.5.9 '@types/draft-js': specifier: ^0.11.20 version: 0.11.20 From 637e8de4bb269ebaf79a86e612b02f47c8c1be06 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 06:44:01 +0000 Subject: [PATCH 3/3] cms-api: Upgrade content-disposition to v3 v3 ships its own type declarations, so the @types/content-disposition dev dependency is no longer needed. It is ESM-only, which the CommonJS build requires the same way it already requires file-type. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CxUhjh9AAnB6AHvGopkdCu --- packages/api/cms-api/package.json | 3 +-- .../api/cms-api/src/dam/files/files.controller.ts | 6 +++--- pnpm-lock.yaml | 13 ++++++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/api/cms-api/package.json b/packages/api/cms-api/package.json index 2c57137c8f0..224052be3fa 100644 --- a/packages/api/cms-api/package.json +++ b/packages/api/cms-api/package.json @@ -54,7 +54,7 @@ "@tiptap/pm": "^3.22.3", "@tiptap/starter-kit": "^3.22.3", "base64url": "^3.0.1", - "content-disposition": "^1.0.1", + "content-disposition": "^3.0.0", "cron-parser": "^3.5.0", "dataloader": "^2.2.3", "date-fns": "^4.4.0", @@ -100,7 +100,6 @@ "@sentry/node": "^9.47.1", "@smithy/types": "^4.14.3", "@swc/core": "^1.15.40", - "@types/content-disposition": "^0.5.9", "@types/draft-js": "^0.11.20", "@types/express": "^5.0.6", "@types/html-to-text": "^9.0.4", diff --git a/packages/api/cms-api/src/dam/files/files.controller.ts b/packages/api/cms-api/src/dam/files/files.controller.ts index a704b343fcf..3f10e78ce1b 100644 --- a/packages/api/cms-api/src/dam/files/files.controller.ts +++ b/packages/api/cms-api/src/dam/files/files.controller.ts @@ -18,7 +18,7 @@ import { } from "@nestjs/common"; import { plainToInstance } from "class-transformer"; import { validate } from "class-validator"; -import contentDisposition from "content-disposition"; +import { create as createContentDisposition } from "content-disposition"; import { Response } from "express"; import { OutgoingHttpHeaders } from "http"; import { basename, extname } from "path"; @@ -240,7 +240,7 @@ export function createFilesController({ Scope: PassedScope, damBasePath }: { Sco throw new ForbiddenException(); } - res.setHeader("Content-Disposition", contentDisposition(file.name)); + res.setHeader("Content-Disposition", createContentDisposition(file.name)); return this.streamFile(file, res, { range, overrideHeaders: { "cache-control": "max-age=31536000, private" } }); // Local caches only (1 year) } @@ -265,7 +265,7 @@ export function createFilesController({ Scope: PassedScope, damBasePath }: { Sco throw new BadRequestException("Content Hash mismatch!"); } - res.setHeader("Content-Disposition", contentDisposition(file.name)); + res.setHeader("Content-Disposition", createContentDisposition(file.name)); return this.streamFile(file, res, { range, overrideHeaders: { "cache-control": "max-age=31536000, s-maxage=86400, public" } }); // Public cache, 1 year for browsers, 1 day for proxies/cdn's } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b634ca5e265..afc2726d7f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2078,8 +2078,8 @@ importers: specifier: ^3.0.1 version: 3.0.1 content-disposition: - specifier: ^1.0.1 - version: 1.0.1 + specifier: ^3.0.0 + version: 3.0.0 cron-parser: specifier: ^3.5.0 version: 3.5.0 @@ -2210,9 +2210,6 @@ importers: '@swc/core': specifier: ^1.15.40 version: 1.15.41 - '@types/content-disposition': - specifier: ^0.5.9 - version: 0.5.9 '@types/draft-js': specifier: ^0.11.20 version: 0.11.20 @@ -10222,6 +10219,10 @@ packages: resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} engines: {node: '>=18'} + content-disposition@3.0.0: + resolution: {integrity: sha512-ZH/0Xs9rMIFWCOmGdmS9eHBTF62qqQYNz4nVjQhkdIO/a0fCP4UIM3mRz/wiqL0L14YgAz/1xio4OaSY4+ON/A==} + engines: {node: '>=22'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -27104,6 +27105,8 @@ snapshots: content-disposition@1.0.1: {} + content-disposition@3.0.0: {} + content-type@1.0.5: {} convert-source-map@1.9.0: {}