diff --git a/frontend/src/__tests__/api/client.test.ts b/frontend/src/__tests__/api/client.test.ts index 5604bc972..9c79f84c5 100644 --- a/frontend/src/__tests__/api/client.test.ts +++ b/frontend/src/__tests__/api/client.test.ts @@ -198,6 +198,22 @@ describe('API Client Auth Header', () => { }); }); +describe('Slicer download URLs', () => { + it('keeps library slicer URLs ending in .3mf when the display name has no extension', () => { + const path = api.getLibrarySlicerDownloadUrl(12, 'token-abc', 'Mecha Mewtwo No AMS Multi Color Parted Statue'); + + expect(path).toBe( + '/api/v1/library/files/12/dl/token-abc/Mecha%20Mewtwo%20No%20AMS%20Multi%20Color%20Parted%20Statue.3mf' + ); + }); + + it('sanitizes library slicer URL filenames before encoding them', () => { + const path = api.getLibrarySlicerDownloadUrl(12, 'token-abc', 'folder/model?bad#name.3mf'); + + expect(path).toBe('/api/v1/library/files/12/dl/token-abc/folder_model_bad_name.3mf'); + }); +}); + describe('FormData requests include auth header', () => { it('importProjectFile includes Authorization header', async () => { // Mock fetch directly for FormData requests (MSW can be flaky with multipart in some environments) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 3bc00b681..3848336fe 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -85,6 +85,11 @@ function parseContentDispositionFilename(header: string | null): string | null { return standardMatch?.[1] || null; } +function buildSlicerUrlFilename(filename: string): string { + const safe = filename.replace(/[/\\?#]/g, '_'); + return safe.toLowerCase().endsWith('.3mf') ? safe : `${safe}.3mf`; +} + async function request( endpoint: string, options: RequestInit = {} @@ -5437,7 +5442,7 @@ export const api = { createLibrarySlicerToken: (fileId: number) => request<{ token: string }>(`/library/files/${fileId}/slicer-token`, { method: 'POST' }), getLibrarySlicerDownloadUrl: (fileId: number, token: string, filename: string) => - `${API_BASE}/library/files/${fileId}/dl/${token}/${encodeURIComponent(filename)}`, + `${API_BASE}/library/files/${fileId}/dl/${token}/${encodeURIComponent(buildSlicerUrlFilename(filename))}`, downloadLibraryFile: async (id: number, filename?: string): Promise => { const headers: Record = {}; if (authToken) {