fix: serve svg+xml as svg (#13277)

Based from https://github.com/payloadcms/payload/pull/13276

Fixes https://github.com/payloadcms/payload/issues/7624

If an uploaded image has `.svg` ext, and the mimeType is read as
`application/xml` adjust the mimeType to `image/svg+xml`.

---------

Co-authored-by: Philipp Schneider <47689073+philipp-tailor@users.noreply.github.com>
This commit is contained in:
Jarrod Flesch
2025-07-25 17:00:51 -04:00
committed by GitHub
parent e8f6cb5ed1
commit bc802846c5
3 changed files with 233 additions and 1 deletions

View File

@@ -93,9 +93,14 @@ export const getFileHandler: PayloadHandler = async (req) => {
const data = streamFile(filePath) const data = streamFile(filePath)
const fileTypeResult = (await fileTypeFromFile(filePath)) || getFileTypeFallback(filePath) const fileTypeResult = (await fileTypeFromFile(filePath)) || getFileTypeFallback(filePath)
let mimeType = fileTypeResult.mime
if (filePath.endsWith('.svg') && fileTypeResult.mime === 'application/xml') {
mimeType = 'image/svg+xml'
}
let headers = new Headers() let headers = new Headers()
headers.set('Content-Type', fileTypeResult.mime) headers.set('Content-Type', mimeType)
headers.set('Content-Length', stats.size + '') headers.set('Content-Length', stats.size + '')
headers = collection.config.upload?.modifyResponseHeaders headers = collection.config.upload?.modifyResponseHeaders
? collection.config.upload.modifyResponseHeaders({ headers }) || headers ? collection.config.upload.modifyResponseHeaders({ headers }) || headers

View File

@@ -42,6 +42,7 @@ import {
mediaWithoutCacheTagsSlug, mediaWithoutCacheTagsSlug,
relationPreviewSlug, relationPreviewSlug,
relationSlug, relationSlug,
svgOnlySlug,
threeDimensionalSlug, threeDimensionalSlug,
withMetadataSlug, withMetadataSlug,
withOnlyJPEGMetadataSlug, withOnlyJPEGMetadataSlug,
@@ -87,6 +88,7 @@ let collectErrorsFromPage: () => boolean
let stopCollectingErrorsFromPage: () => boolean let stopCollectingErrorsFromPage: () => boolean
let bulkUploadsURL: AdminUrlUtil let bulkUploadsURL: AdminUrlUtil
let fileMimeTypeURL: AdminUrlUtil let fileMimeTypeURL: AdminUrlUtil
let svgOnlyURL: AdminUrlUtil
describe('Uploads', () => { describe('Uploads', () => {
let page: Page let page: Page
@@ -126,6 +128,7 @@ describe('Uploads', () => {
constructorOptionsURL = new AdminUrlUtil(serverURL, constructorOptionsSlug) constructorOptionsURL = new AdminUrlUtil(serverURL, constructorOptionsSlug)
bulkUploadsURL = new AdminUrlUtil(serverURL, bulkUploadsSlug) bulkUploadsURL = new AdminUrlUtil(serverURL, bulkUploadsSlug)
fileMimeTypeURL = new AdminUrlUtil(serverURL, fileMimeTypeSlug) fileMimeTypeURL = new AdminUrlUtil(serverURL, fileMimeTypeSlug)
svgOnlyURL = new AdminUrlUtil(serverURL, svgOnlySlug)
const context = await browser.newContext() const context = await browser.newContext()
page = await context.newPage() page = await context.newPage()
@@ -270,6 +273,23 @@ describe('Uploads', () => {
await expect(fileMetaSizeType).toHaveText(/model\/gltf-binary/) await expect(fileMetaSizeType).toHaveText(/model\/gltf-binary/)
}) })
test('should show proper mimetype for svg+xml file', async () => {
await page.goto(svgOnlyURL.create)
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './svgWithXml.svg'))
const filename = page.locator('.file-field__filename')
await expect(filename).toHaveValue('svgWithXml.svg')
await saveDocAndAssert(page)
const fileMetaSizeType = page.locator('.file-meta__size-type')
await expect(fileMetaSizeType).toHaveText(/image\/svg\+xml/)
// ensure the svg loads
const svgImage = page.locator('img[src*="svgWithXml"]')
await expect(svgImage).toBeVisible()
})
test('should create animated file upload', async () => { test('should create animated file upload', async () => {
await page.goto(animatedTypeMediaURL.create) await page.goto(animatedTypeMediaURL.create)

View File

@@ -84,6 +84,9 @@ export interface Config {
'allow-list-media': AllowListMedia; 'allow-list-media': AllowListMedia;
'skip-safe-fetch-media': SkipSafeFetchMedia; 'skip-safe-fetch-media': SkipSafeFetchMedia;
'skip-allow-list-safe-fetch-media': SkipAllowListSafeFetchMedia; 'skip-allow-list-safe-fetch-media': SkipAllowListSafeFetchMedia;
'restrict-file-types': RestrictFileType;
'no-restrict-file-types': NoRestrictFileType;
'no-restrict-file-mime-types': NoRestrictFileMimeType;
'animated-type-media': AnimatedTypeMedia; 'animated-type-media': AnimatedTypeMedia;
enlarge: Enlarge; enlarge: Enlarge;
'without-enlarge': WithoutEnlarge; 'without-enlarge': WithoutEnlarge;
@@ -113,6 +116,8 @@ export interface Config {
'constructor-options': ConstructorOption; 'constructor-options': ConstructorOption;
'bulk-uploads': BulkUpload; 'bulk-uploads': BulkUpload;
'simple-relationship': SimpleRelationship; 'simple-relationship': SimpleRelationship;
'file-mime-type': FileMimeType;
'svg-only': SvgOnly;
users: User; users: User;
'payload-locked-documents': PayloadLockedDocument; 'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference; 'payload-preferences': PayloadPreference;
@@ -137,6 +142,9 @@ export interface Config {
'allow-list-media': AllowListMediaSelect<false> | AllowListMediaSelect<true>; 'allow-list-media': AllowListMediaSelect<false> | AllowListMediaSelect<true>;
'skip-safe-fetch-media': SkipSafeFetchMediaSelect<false> | SkipSafeFetchMediaSelect<true>; 'skip-safe-fetch-media': SkipSafeFetchMediaSelect<false> | SkipSafeFetchMediaSelect<true>;
'skip-allow-list-safe-fetch-media': SkipAllowListSafeFetchMediaSelect<false> | SkipAllowListSafeFetchMediaSelect<true>; 'skip-allow-list-safe-fetch-media': SkipAllowListSafeFetchMediaSelect<false> | SkipAllowListSafeFetchMediaSelect<true>;
'restrict-file-types': RestrictFileTypesSelect<false> | RestrictFileTypesSelect<true>;
'no-restrict-file-types': NoRestrictFileTypesSelect<false> | NoRestrictFileTypesSelect<true>;
'no-restrict-file-mime-types': NoRestrictFileMimeTypesSelect<false> | NoRestrictFileMimeTypesSelect<true>;
'animated-type-media': AnimatedTypeMediaSelect<false> | AnimatedTypeMediaSelect<true>; 'animated-type-media': AnimatedTypeMediaSelect<false> | AnimatedTypeMediaSelect<true>;
enlarge: EnlargeSelect<false> | EnlargeSelect<true>; enlarge: EnlargeSelect<false> | EnlargeSelect<true>;
'without-enlarge': WithoutEnlargeSelect<false> | WithoutEnlargeSelect<true>; 'without-enlarge': WithoutEnlargeSelect<false> | WithoutEnlargeSelect<true>;
@@ -166,6 +174,8 @@ export interface Config {
'constructor-options': ConstructorOptionsSelect<false> | ConstructorOptionsSelect<true>; 'constructor-options': ConstructorOptionsSelect<false> | ConstructorOptionsSelect<true>;
'bulk-uploads': BulkUploadsSelect<false> | BulkUploadsSelect<true>; 'bulk-uploads': BulkUploadsSelect<false> | BulkUploadsSelect<true>;
'simple-relationship': SimpleRelationshipSelect<false> | SimpleRelationshipSelect<true>; 'simple-relationship': SimpleRelationshipSelect<false> | SimpleRelationshipSelect<true>;
'file-mime-type': FileMimeTypeSelect<false> | FileMimeTypeSelect<true>;
'svg-only': SvgOnlySelect<false> | SvgOnlySelect<true>;
users: UsersSelect<false> | UsersSelect<true>; users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>; 'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>; 'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -838,6 +848,60 @@ export interface SkipAllowListSafeFetchMedia {
focalX?: number | null; focalX?: number | null;
focalY?: number | null; focalY?: number | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "restrict-file-types".
*/
export interface RestrictFileType {
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "no-restrict-file-types".
*/
export interface NoRestrictFileType {
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "no-restrict-file-mime-types".
*/
export interface NoRestrictFileMimeType {
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "animated-type-media". * via the `definition` "animated-type-media".
@@ -1502,6 +1566,43 @@ export interface SimpleRelationship {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "file-mime-type".
*/
export interface FileMimeType {
id: string;
title?: string | null;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "svg-only".
*/
export interface SvgOnly {
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users". * via the `definition` "users".
@@ -1601,6 +1702,18 @@ export interface PayloadLockedDocument {
relationTo: 'skip-allow-list-safe-fetch-media'; relationTo: 'skip-allow-list-safe-fetch-media';
value: string | SkipAllowListSafeFetchMedia; value: string | SkipAllowListSafeFetchMedia;
} | null) } | null)
| ({
relationTo: 'restrict-file-types';
value: string | RestrictFileType;
} | null)
| ({
relationTo: 'no-restrict-file-types';
value: string | NoRestrictFileType;
} | null)
| ({
relationTo: 'no-restrict-file-mime-types';
value: string | NoRestrictFileMimeType;
} | null)
| ({ | ({
relationTo: 'animated-type-media'; relationTo: 'animated-type-media';
value: string | AnimatedTypeMedia; value: string | AnimatedTypeMedia;
@@ -1717,6 +1830,14 @@ export interface PayloadLockedDocument {
relationTo: 'simple-relationship'; relationTo: 'simple-relationship';
value: string | SimpleRelationship; value: string | SimpleRelationship;
} | null) } | null)
| ({
relationTo: 'file-mime-type';
value: string | FileMimeType;
} | null)
| ({
relationTo: 'svg-only';
value: string | SvgOnly;
} | null)
| ({ | ({
relationTo: 'users'; relationTo: 'users';
value: string | User; value: string | User;
@@ -2429,6 +2550,57 @@ export interface SkipAllowListSafeFetchMediaSelect<T extends boolean = true> {
focalX?: T; focalX?: T;
focalY?: T; focalY?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "restrict-file-types_select".
*/
export interface RestrictFileTypesSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "no-restrict-file-types_select".
*/
export interface NoRestrictFileTypesSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "no-restrict-file-mime-types_select".
*/
export interface NoRestrictFileMimeTypesSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "animated-type-media_select". * via the `definition` "animated-type-media_select".
@@ -3138,6 +3310,41 @@ export interface SimpleRelationshipSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "file-mime-type_select".
*/
export interface FileMimeTypeSelect<T extends boolean = true> {
title?: T;
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "svg-only_select".
*/
export interface SvgOnlySelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select". * via the `definition` "users_select".