Repository: seanmonstar/reqwest (language: rust)

Issue:
Multipart file with unicode filename
Hi,

I'm tracing a bug to send file via multipart and it seems reqwest implementation doesn't work sometimes when the file name contains special characters (e.g. space, unicode).

The part of code that causes the failure is found at https://docs.rs/reqwest/0.9.5/src/reqwest/multipart.rs.html#405-411.

According to [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2) "Content-Disposition Header Field for Each Part":

>   NOTE: The encoding method described in [RFC5987], which would add a "filename*" parameter to the Content-Disposition header field, MUST NOT be used.

>    Some commonly deployed systems use multipart/form-data with file names directly encoded including octets outside the US-ASCII range. The encoding used for the file names is typically UTF-8, although HTML forms will use the charset associated with the form.

If I understand correctly, this means the implementation should not convert the filename using percent-encoding and just should just leave it as regular quoted utf-8 string.

I tested requesting a unicode-named file using curl, and found it is able to upload the file to the server correctly using "filename=" with a quoted utf-8 string, where reqwest doesn't work for the same task. I traced the raw request using Charles, and found the only difference is on the filename encoding. I also tried to upload a pure ascii filename (without special chars), and reqwest handles it correctly.

I think #252 might be caused by the same issue. Also, in https://github.com/requests/requests/issues/2117 they discussed the same issue in python's requests 2.0 library.


Discussion / hints:

Fix patch:
diff --git a/src/async_impl/multipart.rs b/src/async_impl/multipart.rs
index 58543b681..4455f12fa 100644
--- a/src/async_impl/multipart.rs
+++ b/src/async_impl/multipart.rs
@@ -397,7 +397,7 @@ impl PercentEncoding {
             "Content-Disposition: form-data; {}{}{}",
             self.format_parameter("name", name),
             match field.file_name {
-                Some(ref file_name) => format!("; {}", self.format_parameter("filename", file_name)),
+                Some(ref file_name) => format!("; {}", self.format_filename(file_name)),
                 None => String::new(),
             },
             match field.mime {
@@ -414,6 +414,16 @@ impl PercentEncoding {
         })
     }
 
+    // According to RFC7578 Section 4.2, `filename*=` syntax is invalid.
+    // See https://github.com/seanmonstar/reqwest/issues/419.
+    fn format_filename(&self, filename: &str) -> String {
+        let legal_filename = filename.replace("\\", "\\\\")
+                                     .replace("\"", "\\\"")
+                                     .replace("\r", "\\\r")
+                                     .replace("\n", "\\\n");
+        format!("filename=\"{}\"", legal_filename)
+    }
+
     fn format_parameter(&self, name: &str, value: &str) -> String {
         let legal_value = match *self {
             PercentEncoding::PathSegment => {


Test patch: