Lesstep
Japanese file names · Content-Disposition · Checked October 29, 2026

Japanese file names garbled on upload and download

Your app can break a Japanese file name twice: when the server reads the name the browser uploaded, and when it puts the name back in Content-Disposition for the download. Written as is into that header, 請求書.pdf (“invoice.pdf”) made Node.js 24 throw ERR_INVALID_CHAR. Send it as filename*:

function disposition(name) {
  const ascii = name
    .replace(/[^A-Za-z0-9._-]/g, "_");
  const utf8 = encodeURIComponent(name)
    .replace(/['()*]/g, (c) => "%" +
      c.charCodeAt(0).toString(16)
        .toUpperCase());
  const head = `filename="${ascii}"`;
  return `attachment; ${head}; ` +
    `filename*=UTF-8''${utf8}`;
}

res.setHeader("Content-Disposition",
  disposition("請求書.pdf"));

filename* carries the name as percent-encoded UTF-8 (RFC 8187), and when both are present, RFC 6266 says recipients “SHOULD pick "filename*" and ignore "filename"”. The plain filename is only a fallback for old clients. encodeURIComponent() leaves ' ( ) * as they are, and RFC 8187 doesn't allow them unencoded, so the code encodes them too.

We set the header on a Node.js 24 server for 30 names, from 請求書.pdf to names with spaces, apostrophes, parentheses, %, half-width kana and emoji, fetched it, and percent-decoded each filename* back to the exact name, with no ' ( ) * left unencoded. Written raw, 23 of the 30 made setHeader() throw. We didn't download the files in a browser.

Uploads: read the name as UTF-8

Chromium 152 sent 請求書 2026年10月.pdf in the multipart body as plain UTF-8, and Node.js's built-in Request.formData() read it back exactly. Read as Latin-1, the same bytes give è«æ±æ¸ 2026å¹´10æ.pdf, plus five control characters you can't see. That is what Multer does by default: its README gives defParamCharset the default 'latin1'.

const upload = multer({
  storage,
  defParamCharset: "utf8",
});

// a name already read as Latin-1
const name = Buffer
  .from(file.originalname, "latin1")
  .toString("utf8");

The second form turned our Latin-1 reading back into 請求書 2026年10月.pdf. We didn't run Multer itself; the option is from its README.

Cleaning names: keep the Japanese

A sanitizer that keeps only [a-zA-Z0-9.-] turns 日本語レポート.xlsx into _______.xlsx. Replace only what a path or a file system can't take:

const BAD = /[\p{Cc}/\\:*?"<>|]/gu;

function safeName(name) {
  return name.normalize("NFC")
    .replace(BAD, "_");
}

It kept 日本語レポート.xlsx and 請求書(10月).pdf as they were, turned a/b\c.pdf into a_b_c.pdf, and NFC joined a decomposed が back into one character. Safer still, store the file under an ID and keep the name only for showing and for Content-Disposition.

What went wrong in real apps

The rest of a Japanese check

A user in Japan downloads 請求書.pdf and gets ___.pdf, or the server goes down. The same pass catches Enter while typing Japanese (the fix), yen charged 100 times over (the fix), kanji drawn in their Chinese shapes (the fix), headings that break in the middle of a word (the fix), postal codes rejected when typed in Japanese (the fix), dates saved one day early in Japan (the fix), address forms in the American order (the fix), phone numbers sent to +81 with the 0 still in (the fix), PDFs with the Japanese missing (the fix), search that finds nothing in Japanese (the fix), previews cut down to “...” in Japanese (the fix), lists sorted in the wrong order (the fix), email subjects that arrive garbled (the fix), CSV files that open garbled in Excel (the fix), Japanese titles that give an empty URL slug (the fix), dates in the Japanese era that come out wrong (the fix), full-width spaces that get past your checks (the fix), public holidays your app gets wrong (the fix), and vertical text that comes out sideways or upside down (the fix).

Lesstep translates web apps into Japanese and delivers them as one pull request. In Complete (US$249), we check every screen in Japanese, and upload and download a file with a Japanese name to see that it keeps it. The first step is a free sample.

Node.js is a trademark of the OpenJS Foundation; Python is a registered trademark of the Python Software Foundation; Chromium is a trademark of Google LLC. This page is not affiliated with or endorsed by any of them.

More: Enter sends the message while typing Japanese · Stripe charges 100 times too much in yen · Kanji in their Chinese shapes · Japanese headings that break in the middle of a word · Forms that reject numbers typed in Japanese · Dates that come out wrong for Japanese users · Address forms in the Japanese order · Japanese phone numbers in forms · Japanese text missing from your PDFs · Search that finds nothing in Japanese · Length limits that break in Japanese · Japanese lists sorted in the wrong order · Japanese email subjects that arrive garbled · Japanese CSV files that open garbled in Excel · Japanese titles that give an empty URL slug · Japanese era dates that come out wrong · Full-width spaces that get past your checks · Japanese public holidays your app gets wrong · Japanese vertical text that comes out sideways or upside down · Japanese localization checklist · Japanese localization for your web app