🌐 fix: Percent-encode X-File-Metadata header for Unicode filenames (#12983)

* 🌐 fix: Percent-encode X-File-Metadata header for Unicode filenames

After #12977 preserved Unicode in filenames, the download route
crashes with ERR_INVALID_CHAR because JSON.stringify(file) now
contains non-ASCII characters that Node.js rejects in HTTP headers
per RFC 7230.

Wrap the header value in encodeURIComponent on the server and
decodeURIComponent on the client before JSON.parse.

* fix: Update file route tests after dev merge

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Joseph Licata 2026-05-06 21:01:25 -04:00 committed by GitHub
parent 5c338a4642
commit 5efbcb8b93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 18 additions and 11 deletions

View file

@ -220,14 +220,16 @@ router.delete('/', async (req, res) => {
const agentFiles = files
.filter((f) => toolResourceFiles.includes(f.file_id))
.map((file) => ({ tool_resource: req.body.tool_resource, file_id: file.file_id }));
const accessMap = await hasAccessToFilesViaAgent({
userId: req.user.id,
role: req.user.role,
fileIds: agentFiles.map((file) => file.file_id),
agentId: req.body.agent_id,
isDelete: true,
});
const unauthorizedFiles = agentFiles.filter((file) => !accessMap.get(file.file_id));
const hasAgentEditAccess =
agent.author?.toString() === req.user.id.toString() ||
(await checkPermission({
userId: req.user.id,
role: req.user.role,
resourceType: ResourceType.AGENT,
resourceId: agent._id,
requiredPermission: PermissionBits.EDIT,
}));
const unauthorizedFiles = hasAgentEditAccess ? [] : agentFiles;
if (unauthorizedFiles.length > 0) {
return res.status(403).json({
message: 'You can only delete files you have access to',
@ -519,7 +521,10 @@ router.get('/download/:userId/:file_id', fileAccess, async (req, res) => {
const setHeaders = () => {
res.setHeader('Content-Disposition', getContentDisposition(file.filename));
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('X-File-Metadata', JSON.stringify(getDownloadFileMetadata(file)));
res.setHeader(
'X-File-Metadata',
encodeURIComponent(JSON.stringify(getDownloadFileMetadata(file))),
);
};
if (checkOpenAIStorage(file.source)) {

View file

@ -685,7 +685,7 @@ describe('File Routes - Delete with Agent Access', () => {
expect(response.status).toBe(200);
expect(response.body.toString()).toBe('file content');
expect(response.headers.location).toBeUndefined();
const metadata = JSON.parse(response.headers['x-file-metadata']);
const metadata = JSON.parse(decodeURIComponent(response.headers['x-file-metadata']));
expect(metadata).toMatchObject({
file_id: userFileId,
filename: 'file.pdf',

View file

@ -96,7 +96,9 @@ export const useFileDownload = (
const blob = response.data;
const downloadURL = window.URL.createObjectURL(blob);
try {
const metadata: t.TFile | undefined = JSON.parse(response.headers['x-file-metadata']);
const metadata: t.TFile | undefined = JSON.parse(
decodeURIComponent(response.headers['x-file-metadata']),
);
if (!metadata) {
console.warn('No metadata found for file download', response.headers);
return downloadURL;