問題

有時候我們會透過網頁後端程式直接輸出檔案,因此需要在HTTP Response Header裡面添加Content-Disposition參數,它告訴瀏覽器這個檔案要用瀏覽器直接開啟,例如Content-Disposition: inline。也可以讓瀏覽器給使用者另存新檔,並且提供預設的檔案名稱,例如Content-Disposition: attachment; filename="File.txt"

通常我使用Spring作為網頁伺服器的後端,例如以下使用Spring MVC做一個GET API,用來下載某個檔案。

/** https://klab.tw */
@GetMapping
public ResponseEntity<Resource> downalod() {
    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);
}

然後就會看到Java報錯以下資訊:

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

解決方式

這個不只是Java會遇到,因為HTTP規範中Header只接受ISO-8859-1的字元集,所以無法接受中文、日文、韓文、俄文等各種語言。解決方式URL編碼來將UTF-8字元集的文字,變成網址上常見的百分比開頭的文字,然後filanme要變成filename*=utf-8''

例如filename="檔案名稱.txt"就會變成filename*=utf-8''%E6%AA%94%E6%A1%88%E5%90%8D%E7%A8%B1.txt(不能用雙引號保住檔名),以下直接提供一個快速的轉換方式。

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

然後將上面的Get Method改成以下方式就可以運作囉。

@GetMapping
public ResponseEntity<Resource> downalod() {
    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);
}

這邊提供的是Java搭配Spring的例子,使用其他套件或是其他語言只要尋找對應的URL Encoder方法就可以了。有些URL Encoder會把半形空白變成加號,例如上面的java.net.URLEncoder就是,所以要再把加號變成%20

參考

https://stackoverflow.com/questions/70804280/utf-8-characters-in-filename-for-content-disposition-yield-illegalargumente

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition