the KodeLab

Content-Disposition with Unicode Filenames in Java: Fixing the IllegalArgumentException

322 words 2 min read
Content-Disposition with Unicode Filenames in Java: Fixing the IllegalArgumentException

The problem

Sometimes a web backend streams a file back to the browser directly. That usually means setting Content-Disposition on the HTTP response — Content-Disposition: inline to tell the browser to render it inline, or Content-Disposition: attachment; filename="File.txt" to trigger a download dialog with a pre-filled filename.

A minimal Spring MVC example — a GET endpoint that returns a file:

@GetMapping
public ResponseEntity<Resource> download() {
    String disp = "attachment; filename=\"レポート.txt\"";
    String mime = "text/plain";
    Resource res = new UrlResource(...);
    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION, disp)
        .header(HttpHeaders.CONTENT_TYPE, mime)
        .body(res);
}

Run it and Java throws:

java.lang.IllegalArgumentException: The Unicode character [レ] at code point [12,460]
cannot be encoded as it is outside the permitted range of 0 to 255

The fix

This isn’t just a Java thing. The HTTP spec only allows ISO-8859-1 characters in headers, which rules out CJK, Cyrillic, Greek, and most non-Latin scripts. The fix is to percent-encode the filename as UTF-8 and use the filename*=utf-8'' parameter instead of the plain filename= one.

So filename="レポート.txt" becomes filename*=utf-8''%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88.txt (note: no double quotes around the encoded value). A small helper makes this easy:

public static String filenameEncode(String name) {
    try {
        return java.net.URLEncoder.encode(name, "UTF-8").replace("+", "%20");
    } catch (java.io.UnsupportedEncodingException e) {
        e.printStackTrace();
        return name;
    }
}

Now update the GET method:

@GetMapping
public ResponseEntity<Resource> download() {
    String disp = "attachment; filename*=utf-8''" + filenameEncode("レポート.txt");
    String mime = "text/plain";
    Resource res = new UrlResource(...);
    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION, disp)
        .header(HttpHeaders.CONTENT_TYPE, mime)
        .body(res);
}

The example above is Java with Spring — the same pattern works with any language or framework, as long as you use its URL encoder. One gotcha: some encoders (including java.net.URLEncoder) turn space characters into + rather than %20. That’s valid for query strings but not for Content-Disposition, so replace + with %20 as the helper above does.

References