From 7e458649ef0af8cd2fac5d0f71a2927a2a795e5a Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 22:33:47 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Name=20provisioned=20imag?= =?UTF-8?q?es=20by=20their=20stored=20MIME=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Image uploads are converted to appConfig.imageOutputType while the file record keeps the original filename, so code-env provisioning shipped webp bytes as photo.jpg and extension-sniffing tools mis-handled them. Uploads now rename known converted image types to match the persisted MIME type. --- api/server/services/Files/provision.js | 20 ++++++- api/server/services/Files/provision.spec.js | 58 ++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/api/server/services/Files/provision.js b/api/server/services/Files/provision.js index d836a95606..394ad8527b 100644 --- a/api/server/services/Files/provision.js +++ b/api/server/services/Files/provision.js @@ -29,6 +29,24 @@ async function buildCodeApiHeaders({ apiKey, req }) { }; } +/** Image uploads are converted to appConfig.imageOutputType while the record keeps + * the original filename; rename so sandbox decoders match the stored bytes. */ +function provisionFilename(file) { + if (!file.type?.startsWith('image/')) { + return file.filename; + } + const subtype = file.type.slice('image/'.length); + if (!['webp', 'png', 'jpeg', 'gif'].includes(subtype)) { + return file.filename; + } + const accepted = subtype === 'jpeg' ? ['.jpg', '.jpeg'] : [`.${subtype}`]; + const currentExt = path.extname(file.filename).toLowerCase(); + if (accepted.includes(currentExt)) { + return file.filename; + } + return `${path.basename(file.filename, path.extname(file.filename))}${accepted[0]}`; +} + /** Env var holding the code-execution API key (symmetric with LIBRECHAT_CODE_BASEURL). */ const CODE_API_KEY_FIELD = 'LIBRECHAT_CODE_API_KEY'; @@ -79,7 +97,7 @@ async function provisionToCodeEnv({ req, file, entity_id }) { const uploaded = await uploadCodeEnvFile({ req, stream, - filename: file.filename, + filename: provisionFilename(file), kind, id, }); diff --git a/api/server/services/Files/provision.spec.js b/api/server/services/Files/provision.spec.js index daa1f22026..9221392d29 100644 --- a/api/server/services/Files/provision.spec.js +++ b/api/server/services/Files/provision.spec.js @@ -37,7 +37,8 @@ jest.mock('./strategies', () => ({ const { loadAuthValues } = require('~/server/services/Tools/credentials'); const { getCodeApiAuthHeaders, __codeAxios } = require('@librechat/api'); -const { loadCodeApiKey, checkSessionsAlive } = require('./provision'); +const { getStrategyFunctions } = require('./strategies'); +const { loadCodeApiKey, checkSessionsAlive, provisionToCodeEnv } = require('./provision'); describe('loadCodeApiKey', () => { afterEach(() => jest.clearAllMocks()); @@ -102,3 +103,58 @@ describe('checkSessionsAlive', () => { expect(__codeAxios.mock.calls[0][0].headers['X-API-Key']).toBe('legacy-key'); }); }); + +describe('provisionToCodeEnv', () => { + afterEach(() => jest.clearAllMocks()); + + const setupStrategies = (uploadCodeEnvFile) => { + const getDownloadStream = jest.fn().mockResolvedValue({ pipe: jest.fn() }); + getStrategyFunctions.mockImplementation((source) => + source === 'execute_code' ? { handleFileUpload: uploadCodeEnvFile } : { getDownloadStream }, + ); + }; + + it('renames converted images to match the stored MIME type', async () => { + const uploadCodeEnvFile = jest + .fn() + .mockResolvedValue({ storage_session_id: 's1', file_id: 'r1' }); + setupStrategies(uploadCodeEnvFile); + + const result = await provisionToCodeEnv({ + req: { user: { id: 'u1' } }, + file: { + file_id: 'f1', + filename: 'photo.jpg', + type: 'image/webp', + source: 'local', + filepath: '/x/photo.jpg', + metadata: {}, + }, + }); + + expect(uploadCodeEnvFile).toHaveBeenCalledWith( + expect.objectContaining({ filename: 'photo.webp' }), + ); + expect(result.codeEnvRef.file_id).toBe('r1'); + }); + + it('keeps filenames untouched when the extension already matches or the file is not an image', async () => { + const uploadCodeEnvFile = jest + .fn() + .mockResolvedValue({ storage_session_id: 's1', file_id: 'r1' }); + setupStrategies(uploadCodeEnvFile); + const baseFile = { file_id: 'f1', source: 'local', filepath: '/x/f', metadata: {} }; + + await provisionToCodeEnv({ + req: { user: { id: 'u1' } }, + file: { ...baseFile, filename: 'data.csv', type: 'text/csv' }, + }); + await provisionToCodeEnv({ + req: { user: { id: 'u1' } }, + file: { ...baseFile, filename: 'pic.webp', type: 'image/webp' }, + }); + + expect(uploadCodeEnvFile.mock.calls[0][0].filename).toBe('data.csv'); + expect(uploadCodeEnvFile.mock.calls[1][0].filename).toBe('pic.webp'); + }); +});