Lỗi xảy ra khi sử dụng HttpClient upload file có tên tiếng Nhật

HttpClient là thư viện java tương tác với server thông qua giao thức http.

Phiên bản mới nhất là 4.4 có thể tải tại trang web http://hc.apache.org/

Tôi sử HttpClient để viết 1 chương trình upload file csv lên server. Dưới đây là đoạn mã tạo ra HttpEntity trong đó bao 1 gồm trường text và 1 file đọc từ ổ cứng.

	/*
	 * prepare request entity
	 */
	public HttpEntity prepareEntity (File file) throws ParseException, IOException {
		ContentType contentType = ContentType.create("text/plain", Consts.UTF_8);
		MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
		HttpEntity reqEntity = entityBuilder
				.addTextBody("site[secret]", secret, contentType)
				.addBinaryBody("site[file]", file)
				.build();
		return reqEntity;
	}

Mọi thứ có vẻ ổn cho đến khi khách hàng người Nhật gửi 1 file có tên tiếng Nhật. Xem log bên phía server có thể thấy tất cả tiếng Nhật đều bị thay bởi dấu "?".

pry(#<Api::V1::UserAttributesController>)> params
=> {
"file"=>
    #<ActionDispatch::Http::UploadedFile:0x007f9d92132450
     @content_type="application/octet-stream",
     @headers="Content-Disposition: form-data; name=\"site[file]\"; filename=\" ????????.txt\"\r\nContent-Type: application/octet-stream\r\n",
     @original_filename=" ????????.txt",
     @tempfile=#<File:/var/folders/pl/brkj_p2n1vvd_cpf5f1bpb6c0000gn/T/RackMultipart20150512-959-1i29fcw>>}
}

Chuyện gì xảy ra vậy? Chẳng lẽ HttpClient không hỗ trợ UTF-8. Xem ví dụ và tài liệu của Apache HttpClient cũng không thấy đề cập đến vấn đề này. Hỏi nhà thông thái Google cả ngày trời cũng không tìm ra câu trả lời. 😦

Bất đắc dĩ tôi đành phải lang thang trên sa mạc code của HttpClient. Đang trong cơn đói khát, tôi đã phát hiện ra ốc đảo trù phú.

    @Override
    protected void formatMultipartHeader(
        final FormBodyPart part,
        final OutputStream out) throws IOException {

        // For RFC6532, we output all fields with UTF-8 encoding.
        final Header header = part.getHeader();
        for (final MinimalField field: header) {
            writeField(field, MIME.UTF8_CHARSET, out);
        }
    }

"For RFC6532, we output all fields with UTF-8 encoding". Thật khó có thể diễn tả hết cảm xúc khi nhìn thấy dòng này. Thì ra HttpClient viết sẵn 3 HttpMultipartMode. HttpMultipartMode.RFC6532 được viết riêng để hỗ trợ UTF-8. Và cách sửa đơn giản đến mức bất ngờ là chỉ cần thêm RFC6532 mode cho HttpEntity.

	public HttpEntity prepareEntity (File file) throws ParseException, IOException {
		ContentType contentType = ContentType.create("text/plain", Consts.UTF_8);
		MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
		HttpEntity reqEntity = entityBuilder
				.setMode(HttpMultipartMode.RFC6532)
				.addTextBody("site[secret]", secret, contentType)
				.addBinaryBody("site[file]", file)
				.build();
		return reqEntity;
	}

Kiểm tra lại request bên phía server:

pry(#<Api::V1::UserAttributesController>)> params
=> {
   "file"=>
    #<ActionDispatch::Http::UploadedFile:0x007f9d8ac7e618
     @content_type="application/octet-stream",
     @headers=
      "Content-Disposition: form-data; name=\"site[file]\"; filename=\" \xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E\xE3\x81\xA7\xE3\x81\x8D\xE3\x81\xBE\xE3\x81\x97\xE3\x81\x9F.txt\"\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: binary\r\n",
     @original_filename=" 日本語できました.txt",
     @tempfile=#<File:/var/folders/pl/brkj_p2n1vvd_cpf5f1bpb6c0000gn/T/RackMultipart20150512-959-1na3nuw>>
     }

original_filename đã là tiếng Nhật nhưng headers có vẻ vẫn chưa đúng. Nhưng đây không phải lỗi của HttpClient mà là lỗi bên phía rails server. Cách sửa lỗi này tôi sẽ đề cập trong bài viết khác.