Published: August 26, 2026 · by Srinu Desetti · expressjs/multer#1421 (opens in a new tab) · merged August 25, 2026
Fixing Escaped Upload Filenames in Multer
Multer is the standard middleware for handling file uploads in Express. My contribution — merged upstream — fixes a bug where uploaded filenames reached applications still escaped: a file named file".ext arrived as file%22.ext in req.file.originalname.
The Problem
Upload a file named:
file".extApplications received:
req.file.originalname
// "file%22.ext" ❌ — escaped
// expected:
// 'file".ext' ✅Anything built on the original filename — display names, stored metadata, downloads — carried the wrong name.
Where the Escaping Comes From
When a browser sends a file through multipart/form-data, the filename travels inside a quoted header value:
Content-Disposition: form-data; name="file"; filename="file%22.ext"A raw " would end the quoted value early; raw line breaks would end the header line itself. So the WHATWG HTML standard requires browsers to escape exactly three bytes in multipart filenames:
| Byte | Character | Escaped as |
|---|---|---|
0x0A | \n line feed | %0A |
0x0D | \r carriage return | %0D |
0x22 | " double quote | %22 |
That's the complete list — the browser escapes those three and nothing else. (Both \n and \r matter because Windows line endings are \r\n, which serializes as %0D%0A.)
Busboy — Multer's multipart parser — passes the filename through still escaped. Multer exposed it as-is. That was the bug.
The Fix: A Selective Decoder
How it works, step by step:
- The regex
/%0A|%0D|%22/gimatches any of the three escape sequences —greplaces all occurrences,iaccepts lowercase (%0a) too. - The callback runs once per match, receiving the matched text.
toUpperCase()normalizes the match so thecaselabels compare consistently.- The mapping:
%0A→ newline,%0D→ carriage return, and the only remaining possibility —%22— becomes"in thedefaultcase.
Applied where Multer builds the file object:
Result:
file%22.ext → file".ext
hello%0D%0Aworld.txt → "hello\r\nworld.txt"The Critical Design Decision: Why Not decodeURIComponent()?
The obvious one-liner would be:
originalname: decodeURIComponent(filename) // ❌ WRONGBut a filename can legitimately contain a literal percent sign:
50%off.pdfThe browser never escaped that % — it's part of the real name. A general URL decoder would treat percent sequences that were never encoding as if they were:
The principle: reverse exactly the escaping the sender performed — never more. The browser escapes three sequences, so the decoder decodes three sequences. Decoding anything else corrupts filenames that were never encoded.
The Tests
The regression tests cover each character and the edge cases:
%22→",%0A→\n,%0D→\r%0D%0A→\r\n(Windows-style line ending)- Literal
%preserved untouched (50%off.pdfstays50%off.pdf) - Lowercase escapes (
%0a,%0d) decode too
Impact
req.file.originalnamenow matches the file the user actually uploaded — for every application using Multer, filenames with quotes or line breaks arrive correctly instead of escaped.- Standards-correct behavior: Multer now reverses exactly the escaping the WHATWG standard requires browsers to perform.
- No collateral damage: filenames with literal percent signs are untouched — the selective decoder can't corrupt them the way a general URL decoder would.
What I Contributed
- Identified that the escaped filenames came from WHATWG-mandated browser escaping that was never reversed.
- Implemented the selective decoder for exactly the three escaped sequences — deliberately avoiding
decodeURIComponentto preserve literal%in filenames. - Added regression tests covering all three characters, the
%0D%0Acombination, and literal-percent preservation. - Worked with the Multer maintainers through review until the PR was approved and merged.
Linked issue: expressjs/multer#836 (opens in a new tab)
View the pull request → expressjs/multer#1421 (opens in a new tab)
← Previous: Tailwind CSS invalid modifiers · All contributions →
Frequently Asked Questions
Why were uploaded filenames escaped in the first place?
Because the filename travels inside a quoted header value in multipart/form-data, the WHATWG HTML standard requires browsers to escape exactly three bytes — line feed (%0A), carriage return (%0D), and double quote (%22) — so they can't break the header structure. Multer's parser passed the escaped form through, and Multer never reversed it.
Why not just use decodeURIComponent?
Filenames can contain literal percent signs — 50%off.pdf is a valid filename the browser never escaped. decodeURIComponent would throw a URIError on it or decode sequences like %20 that were never encoding. The fix decodes only the three sequences browsers actually escape, so real percent signs survive.
Why do both %0A and %0D need handling?
They're the two halves of line endings: line feed and carriage return. Windows-style line endings are \r\n, which serializes as %0D%0A — so a filename containing a line break can carry either or both sequences.
Does this change break existing applications?
It corrects them. Applications were receiving escaped names like file%22.ext for files actually named file".ext. After the fix, originalname matches the real filename; filenames without quotes or line breaks — the overwhelming majority — are completely unaffected.