Summary
Files uploaded via POST /upload and POST /upload/batch lose their non-ASCII
(e.g. Cyrillic) filenames. The name arrives in the sandbox /mnt/data as
UTF-8-bytes-decoded-as-Latin-1 mojibake.
Example: Расчет_иска.docx → РаÑÑеÑ_иÑка.docx
Affected version
main (Initial public release, commit 3fa1f6c). Reproduced with busboy@1.6.0.
Root cause
service/src/service/router.ts builds busboy without defParamCharset, so
busboy 1.x falls back to its latin1 default and decodes the multipart
Content-Disposition filename bytes as Latin-1. Clients send the filename as
raw UTF-8 bytes (the de-facto browser behavior; LibreChat does this via
form-data), so the bytes are misread.
Two call sites:
/upload — service/src/service/router.ts (~L334)
/upload/batch — service/src/service/router.ts (~L531)
The sibling handler service/src/file-server.ts (POST /sessions/:id/objects)
already sets defParamCharset: 'utf8' — the fix landed there but was missed
on the two router.ts upload routes. Once mangled, the name is preserved
verbatim downstream (X-Original-Filename: encodeURIComponent(filename) →
base64 object metadata → filename*=UTF-8''… → written to /mnt/data), so the
corruption is baked in at busboy parse time.
Minimal reproduction
// node, busboy@1.6.0, form-data@4.0.6
const Busboy = require('busboy');
const FormData = require('form-data');
const { Readable } = require('stream');
const NAME = 'Расчет_иска.docx';
const form = new FormData();
form.append('file', Buffer.from('x'), { filename: NAME });
const chunks = [];
form.on('data', c => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c, 'utf8')));
form.on('end', () => {
const body = Buffer.concat(chunks);
const parse = opts => new Promise(res => {
const bb = Busboy({ headers: form.getHeaders(), ...opts });
bb.on('file', (_f, s, info) => { console.log(JSON.stringify(info.filename)); s.resume(); });
bb.on('close', res);
Readable.from(body).pipe(bb);
});
(async () => {
process.stdout.write('default (latin1): '); await parse({});
process.stdout.write("defParamCharset utf8: "); await parse({ defParamCharset: 'utf8' });
})();
});
Output:
default (latin1): "РаÑÑеÑ_иÑка.docx"
defParamCharset utf8: "Расчет_иска.docx"
Expected vs actual
- Expected:
/mnt/data/Расчет_иска.docx
- Actual:
/mnt/data/РаÑÑеÑ_иÑка.docx
Proposed fix
Add defParamCharset: 'utf8' to both busboy() calls in router.ts, matching
file-server.ts:
const bb = busboy({
headers: req.headers,
limits: { fileSize: planFileSize },
+ defParamCharset: 'utf8',
preservePath: true,
});
(and the same for the /upload/batch handler).
Summary
Files uploaded via
POST /uploadandPOST /upload/batchlose their non-ASCII(e.g. Cyrillic) filenames. The name arrives in the sandbox
/mnt/dataasUTF-8-bytes-decoded-as-Latin-1 mojibake.
Example:
Расчет_иска.docx→РаÑÑеÑ_иÑка.docxAffected version
main(Initial public release, commit3fa1f6c). Reproduced withbusboy@1.6.0.Root cause
service/src/service/router.tsbuilds busboy withoutdefParamCharset, sobusboy 1.x falls back to its
latin1default and decodes the multipartContent-Dispositionfilename bytes as Latin-1. Clients send the filename asraw UTF-8 bytes (the de-facto browser behavior; LibreChat does this via
form-data), so the bytes are misread.Two call sites:
/upload—service/src/service/router.ts(~L334)/upload/batch—service/src/service/router.ts(~L531)The sibling handler
service/src/file-server.ts(POST /sessions/:id/objects)already sets
defParamCharset: 'utf8'— the fix landed there but was missedon the two
router.tsupload routes. Once mangled, the name is preservedverbatim downstream (
X-Original-Filename: encodeURIComponent(filename)→base64 object metadata →
filename*=UTF-8''…→ written to/mnt/data), so thecorruption is baked in at busboy parse time.
Minimal reproduction
Output:
Expected vs actual
/mnt/data/Расчет_иска.docx/mnt/data/РаÑÑеÑ_иÑка.docxProposed fix
Add
defParamCharset: 'utf8'to bothbusboy()calls inrouter.ts, matchingfile-server.ts:const bb = busboy({ headers: req.headers, limits: { fileSize: planFileSize }, + defParamCharset: 'utf8', preservePath: true, });(and the same for the
/upload/batchhandler).