Repository: seanmonstar/reqwest (language: rust)

Issue:
Add multipart/form-data support
Inspired by voider1's implementation in #143 . Fix #4 

The most important feature of this version is that the request body is implemented as Read so that it does not need to be stored in memory when files are sent.

Additionally the API is different. I just went with what felt right to me because I had to change most things from voider1 anyways:
In this version there is no additional MultipartRequestBuilder struct, instead there is a multipart method (like json or form) which sets the body of the request appropriately.

There are still many things marked as TODO in comments in the code where I wasnt sure what the best way to proceed was.

I also need to add tests, so far I have only written but never ran it.


We can probably cherry pick the best ideas from both pull requests. I also looked into using existing multipart crates but to me it seems like they cant easily be used none of them implement Read.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 3cdae3f9f..23b85bfb2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,6 +16,7 @@ hyper = "0.11"
 hyper-tls = "0.1.2"
 libflate = "0.1.11"
 log = "0.3"
+mime_guess = "2.0.0-alpha.2"
 native-tls = "0.1.3"
 serde = "1.0"
 serde_json = "1.0"
@@ -24,11 +25,12 @@ tokio-core = "0.1.6"
 tokio-io = "0.1"
 tokio-tls = "0.1"
 url = "1.2"
+uuid = { version = "0.5", features = ["v4"] }
 
 [dev-dependencies]
 env_logger = "0.4"
-serde_derive = "1.0"
 error-chain = "0.10"
+serde_derive = "1.0"
 
 [features]
 unstable = []
diff --git a/src/body.rs b/src/body.rs
index 1b55f49db..c673f624e 100644
--- a/src/body.rs
+++ b/src/body.rs
@@ -1,6 +1,6 @@
-use std::io::{self, Read};
 use std::fs::File;
 use std::fmt;
+use std::io::{self, Cursor, Read};
 
 use bytes::Bytes;
 use hyper::{self, Chunk};
@@ -16,7 +16,7 @@ use {async_impl, wait};
 /// [builder]: ./struct.RequestBuilder.html#method.body
 #[derive(Debug)]
 pub struct Body {
-    reader: Kind,
+    kind: Kind,
 }
 
 impl Body {
@@ -54,7 +54,7 @@ impl Body {
     /// ```
     pub fn new<R: Read + Send + 'static>(reader: R) -> Body {
         Body {
-            reader: Kind::Reader(Box::new(reader), None),
+            kind: Kind::Reader(Box::from(reader), None),
         }
     }
 
@@ -74,21 +74,11 @@ impl Body {
     /// ```
     pub fn sized<R: Read + Send + 'static>(reader: R, len: u64) -> Body {
         Body {
-            reader: Kind::Reader(Box::new(reader), Some(len)),
+            kind: Kind::Reader(Box::from(reader), Some(len)),
         }
     }
 }
 
-// useful for tests, but not publicly exposed
-#[cfg(test)]
-pub fn read_to_string(mut body: Body) -> ::std::io::Result<String> {
-    let mut s = String::new();
-    match body.reader {
-            Kind::Reader(ref mut reader, _) => reader.read_to_string(&mut s),
-            Kind::Bytes(ref mut bytes) => (&**bytes).read_to_string(&mut s),
-        }
-        .map(|_| s)
-}
 
 enum Kind {
     Reader(Box<Read + Send>, Option<u64>),
@@ -99,7 +89,7 @@ impl From<Vec<u8>> for Body {
     #[inline]
     fn from(v: Vec<u8>) -> Body {
         Body {
-            reader: Kind::Bytes(v.into()),
+            kind: Kind::Bytes(v.into()),
         }
     }
 }
@@ -116,7 +106,7 @@ impl From<&'static [u8]> for Body {
     #[inline]
     fn from(s: &'static [u8]) -> Body {
         Body {
-            reader: Kind::Bytes(Bytes::from_static(s)),
+            kind: Kind::Bytes(Bytes::from_static(s)),
         }
     }
 }
@@ -133,7 +123,7 @@ impl From<File> for Body {
     fn from(f: File) -> Body {
         let len = f.metadata().map(|m| m.len()).ok();
         Body {
-            reader: Kind::Reader(Box::new(f), len),
+            kind: Kind::Reader(Box::new(f), len),
         }
     }
 }
@@ -141,8 +131,21 @@ impl From<File> for Body {
 impl fmt::Debug for Kind {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match *self {
-            Kind::Reader(_, ref v) => f.debug_tuple("Kind::Reader").field(&"_").field(v).finish(),
-            Kind::Bytes(ref v) => f.debug_tuple("Kind::Bytes").field(v).finish(),
+            Kind::Reader(_, ref v) => f.debug_struct("Reader")
+                .field("length", &DebugLength(v))
+                .finish(),
+            Kind::Bytes(ref v) => fmt::Debug::fmt(v, f),
+        }
+    }
+}
+
+struct DebugLength<'a>(&'a Option<u64>);
+
+impl<'a> fmt::Debug for DebugLength<'a> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match *self.0 {
+            Some(ref len) => fmt::Debug::fmt(len, f),
+            None => f.write_str("Unknown"),
         }
     }
 }
@@ -150,6 +153,35 @@ impl fmt::Debug for Kind {
 
 // pub(crate)
 
+pub fn len(body: &Body) -> Option<u64> {
+    match body.kind {
+        Kind::Reader(_, len) => len,
+        Kind::Bytes(ref bytes) => Some(bytes.len() as u64),
+    }
+}
+
+pub enum Reader {
+    Reader(Box<Read + Send>),
+    Bytes(Cursor<Bytes>),
+}
+
+#[inline]
+pub fn reader(body: Body) -> Reader {
+    match body.kind {
+        Kind::Reader(r, _) => Reader::Reader(r),
+        Kind::Bytes(b) => Reader::Bytes(Cursor::new(b)),
+    }
+}
+
+impl Read for Reader {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        match *self {
+            Reader::Reader(ref mut rdr) => rdr.read(buf),
+            Reader::Bytes(ref mut rdr) => rdr.read(buf),
+        }
+    }
+}
+
 pub struct Sender {
     body: (Box<Read + Send>, Option<u64>),
     tx: wait::WaitSink<::futures::sync::mpsc::Sender<hyper::Result<Chunk>>>,
@@ -193,7 +225,7 @@ impl Sender {
 
 #[inline]
 pub fn async(body: Body) -> (Option<Sender>, async_impl::Body, Option<u64>) {
-    match body.reader {
+    match body.kind {
         Kind::Reader(read, len) => {
             let (tx, rx) = hyper::Body::pair();
             let tx = Sender {
@@ -208,3 +240,14 @@ pub fn async(body: Body) -> (Option<Sender>, async_impl::Body, Option<u64>) {
         }
     }
 }
+
+// useful for tests, but not publicly exposed
+#[cfg(test)]
+pub fn read_to_string(mut body: Body) -> io::Result<String> {
+    let mut s = String::new();
+    match body.kind {
+            Kind::Reader(ref mut reader, _) => reader.read_to_string(&mut s),
+            Kind::Bytes(ref mut bytes) => (&**bytes).read_to_string(&mut s),
+        }
+        .map(|_| s)
+}
diff --git a/src/lib.rs b/src/lib.rs
index 947d0ecc3..1b7ec6a79 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -11,7 +11,7 @@
 //! to do for them.
 //!
 //! - Uses system-native TLS
-//! - Plain bodies, JSON, urlencoded, (TODO: multipart)
+//! - Plain bodies, JSON, urlencoded, multipart
 //! - Customizable redirect policy
 //! - Proxies
 //! - (TODO: Cookies)
@@ -127,6 +127,7 @@ extern crate hyper_tls;
 #[macro_use]
 extern crate log;
 extern crate libflate;
+extern crate mime_guess;
 extern crate native_tls;
 extern crate serde;
 extern crate serde_json;
@@ -135,6 +136,7 @@ extern crate tokio_core;
 extern crate tokio_io;
 extern crate tokio_tls;
 extern crate url;
+extern crate uuid;
 
 pub use hyper::header;
 pub use hyper::mime;
@@ -158,6 +160,21 @@ pub use self::tls::Certificate;
 #[macro_use]
 mod error;
 
+mod async_impl;
+mod connect;
+mod body;
+mod client;
+mod into_url;
+mod multipart_;
+mod proxy;
+mod redirect;
+mod request;
+mod response;
+mod tls;
+mod wait;
+
+pub mod multipart;
+
 /// A set of unstable functionality.
 ///
 /// This module is only available when the `unstable` feature is enabled.
@@ -182,20 +199,6 @@ pub mod unstable {
     }
 }
 
-
-mod async_impl;
-mod connect;
-mod body;
-mod client;
-mod into_url;
-mod proxy;
-mod redirect;
-mod request;
-mod response;
-mod tls;
-mod wait;
-
-
 /// Shortcut method to quickly make a `GET` request.
 ///
 /// See also the methods on the [`reqwest::Response`](./struct.Response.html)
diff --git a/src/multipart.rs b/src/multipart.rs
new file mode 100644
index 000000000..bd0321ea7
--- /dev/null
+++ b/src/multipart.rs
@@ -0,0 +1,2 @@
+//! # multipart/form-data
+pub use ::multipart_::{Form, Part};
diff --git a/src/multipart_.rs b/src/multipart_.rs
new file mode 100644
index 000000000..9ec771148
--- /dev/null
+++ b/src/multipart_.rs
@@ -0,0 +1,437 @@
+use std::borrow::Cow;
+use std::fmt;
+use std::fs::File;
+use std::io::{self, Cursor, Read};
+use std::path::Path;
+
+use mime::Mime;
+use mime_guess;
+use url::percent_encoding;
+use uuid::Uuid;
+
+use {Body};
+
+/// A multipart/form-data request.
+pub struct Form {
+    boundary: String,
+    fields: Vec<(Cow<'static, str>, Part)>,
+    headers: Vec<String>,
+}
+
+impl Form {
+    /// Creates a new Form without any content.
+    pub fn new() -> Form {
+        Form {
+            boundary: format!("{}", Uuid::new_v4().simple()),
+            fields: Vec::new(),
+            headers: Vec::new(),
+        }
+    }
+
+    /// Get the boundary that this form will use.
+    #[inline]
+    pub fn boundary(&self) -> &str {
+        &self.boundary
+    }
+
+    /// Add a data field with supplied name and value.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// let form = reqwest::multipart::Form::new()
+    ///     .text("username", "seanmonstar")
+    ///     .text("password", "secret");
+    /// ```
+    pub fn text<T, U>(self, name: T, value: U) -> Form
+    where T: Into<Cow<'static, str>>,
+          U: Into<Cow<'static, str>>,
+    {
+        self.part(name, Part::text(value))
+    }
+
+    /// Adds a file field.
+    ///
+    /// The path will be used to try to guess the filename and mime.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// # fn run() -> ::std::io::Result<()> {
+    /// let files = reqwest::multipart::Form::new()
+    ///     .file("key", "/path/to/file")?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// # Errors
+    ///
+    /// Errors when the file cannot be opened.
+    pub fn file<T, U>(self, name: T, path: U) -> io::Result<Form>
+    where T: Into<Cow<'static, str>>,
+          U: AsRef<Path>
+    {
+        Ok(self.part(name, Part::file(path)?))
+    }
+
+    /// Adds a customized Part.
+    pub fn part<T>(mut self, name: T, part: Part) -> Form
+    where T: Into<Cow<'static, str>>,
+    {
+        self.fields.push((name.into(), part));
+        self
+    }
+}
+
+impl Form {
+    fn reader(self) -> Reader {
+        Reader::new(self)
+    }
+
+    fn compute_length(&mut self) -> Option<u64> {
+        let mut length = 0u64;
+        for &(ref name, ref field) in self.fields.iter() {
+            match ::body::len(&field.value) {
+                Some(value_length) => {
+                    // We are constructing the header just to get its length. To not have to
+                    // construct it again when the request is sent we cache these headers.
+                    let header = header(name, field);
+                    let header_length = header.len();
+                    self.headers.push(header);
+                    // The additions mimick the format string out of which the field is constructed
+                    // in Reader. Not the cleanest solution because if that format string is
+                    // ever changed then this formula needs to be changed too which is not an
+                    // obvious dependency in the code.
+                    length += 2 + self.boundary.len() as u64 + 2 + header_length as u64 + 4 + value_length + 2
+                }
+                _ => return None,
+            }
+        }
+        // If there is a at least one field there is a special boundary for the very last field.
+        if self.fields.len() != 0 {
+            length += 2 + self.boundary.len() as u64 + 2
+        }
+        Some(length)
+    }
+}
+
+impl fmt::Debug for Form {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("Form")
+            .field("boundary", &self.boundary)
+            .field("parts", &self.fields)
+            .finish()
+    }
+}
+
+
+/// A field in a multipart form.
+pub struct Part {
+    value: Body,
+    mime: Option<Mime>,
+    file_name: Option<Cow<'static, str>>,
+}
+
+impl Part {
+    /// Makes a text parameter.
+    pub fn text<T>(value: T) -> Part
+    where T: Into<Cow<'static, str>>,
+    {
+        let body = match value.into() {
+            Cow::Borrowed(slice) => Body::from(slice),
+            Cow::Owned(string) => Body::from(string),
+        };
+        Part::new(body)
+    }
+
+    /// Adds a generic reader.
+    ///
+    /// Does not set filename or mime.
+    pub fn reader<T: Read + Send + 'static>(value: T) -> Part {
+        Part::new(Body::new(value))
+    }
+
+    /// Adds a generic reader with known length.
+    ///
+    /// Does not set filename or mime.
+    pub fn reader_with_length<T: Read + Send + 'static>(value: T, length: u64) -> Part {
+        Part::new(Body::sized(value, length))
+    }
+
+    /// Makes a file parameter.
+    ///
+    /// # Errors
+    ///
+    /// Errors when the file cannot be opened.
+    pub fn file<T: AsRef<Path>>(path: T) -> io::Result<Part> {
+        let path = path.as_ref();
+        let file_name = path.file_name().and_then(|filename| {
+            Some(Cow::from(filename.to_string_lossy().into_owned()))
+        });
+        let ext = path.extension()
+            .and_then(|ext| ext.to_str())
+            .unwrap_or("");
+        let mime = mime_guess::get_mime_type(ext);
+        let file = File::open(path)?;
+        let mut field = Part::new(Body::from(file));
+        field.mime = Some(mime);
+        field.file_name = file_name;
+        Ok(field)
+    }
+
+    fn new(value: Body) -> Part {
+        Part {
+            value: value,
+            mime: None,
+            file_name: None,
+        }
+    }
+
+    /// Sets the mime, builder style.
+    pub fn mime(mut self, mime: Mime) -> Part {
+        self.mime = Some(mime);
+        self
+    }
+
+    /// Sets the filename, builder style.
+    pub fn file_name<T: Into<Cow<'static, str>>>(mut self, filename: T) -> Part {
+        self.file_name = Some(filename.into());
+        self
+    }
+}
+
+impl fmt::Debug for Part {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("Part")
+            .field("value", &self.value)
+            .field("mime", &self.mime)
+            .field("file_name", &self.file_name)
+            .finish()
+    }
+}
+
+// pub(crate)
+
+// Turns this Form into a Reader which implements the Read trait.
+pub fn reader(form: Form) -> Reader {
+    form.reader()
+}
+
+// If predictable, computes the length the request will have
+// The length should be preditable if only String and file fields have been added,
+// but not if a generic reader has been added;
+pub fn compute_length(form: &mut Form) -> Option<u64> {
+    form.compute_length()
+}
+
+pub fn boundary(form: &Form) -> &str {
+    &form.boundary
+}
+
+pub struct Reader {
+    form: Form,
+    active_reader: Option<Box<Read + Send>>,
+}
+
+impl fmt::Debug for Reader {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("Reader")
+            .field("form", &self.form)
+            .finish()
+    }
+}
+
+impl Reader {
+    fn new(form: Form) -> Reader {
+        let mut reader = Reader {
+            form: form,
+            active_reader: None,
+        };
+        reader.next_reader();
+        reader
+    }
+
+    fn next_reader(&mut self) {
+        self.active_reader = if self.form.fields.len() != 0 {
+            // We need to move out of the vector here because we are consuming the field's reader
+            let (name, field) = self.form.fields.remove(0);
+            let reader = Cursor::new(format!(
+                "--{}\r\n{}\r\n\r\n",
+                self.form.boundary,
+                // Try to use cached headers created by compute_length
+                if self.form.headers.len() > 0 {
+                    self.form.headers.remove(0)
+                } else {
+                    header(&name, &field)
+                }
+            )).chain(::body::reader(field.value))
+                .chain(Cursor::new("\r\n"));
+            // According to https://tools.ietf.org/html/rfc2046#section-5.1.1
+            // the very last field has a special boundary
+            if self.form.fields.len() != 0 {
+                Some(Box::new(reader))
+            } else {
+                Some(Box::new(reader.chain(Cursor::new(
+                    format!("--{}--", self.form.boundary),
+                ))))
+            }
+        } else {
+            None
+        }
+    }
+}
+
+impl Read for Reader {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        let mut total_bytes_read = 0usize;
+        let mut last_read_bytes;
+        loop {
+            match self.active_reader {
+                Some(ref mut reader) => {
+                    last_read_bytes = reader.read(&mut buf[total_bytes_read..])?;
+                    total_bytes_read += last_read_bytes;
+                    if total_bytes_read == buf.len() {
+                        return Ok(total_bytes_read);
+                    }
+                }
+                None => return Ok(total_bytes_read),
+            };
+            if last_read_bytes == 0 && buf.len() != 0 {
+                self.next_reader();
+            }
+        }
+    }
+}
+
+
+fn header(name: &str, field: &Part) -> String {
+    format!(
+        "Content-Disposition: form-data; {}{}{}",
+        format_parameter("name", name),
+        match field.file_name {
+            Some(ref file_name) => format!("; {}", format_parameter("filename", file_name)),
+            None => String::new(),
+        },
+        match field.mime {
+            Some(ref mime) => format!("\r\nContent-Type: {}", mime),
+            None => "".to_string(),
+        }
+    )
+}
+
+fn format_parameter(name: &str, value: &str) -> String {
+    let legal_value =
+        percent_encoding::utf8_percent_encode(value, percent_encoding::PATH_SEGMENT_ENCODE_SET)
+            .to_string();
+    if value.len() == legal_value.len() {
+        // nothing has been percent encoded
+        format!("{}=\"{}\"", name, value)
+    } else {
+        // something has been percent encoded
+        format!("{}*=utf-8''{}", name, legal_value)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn form_empty() {
+        let mut output = Vec::new();
+        let mut form = Form::new();
+        let length = form.compute_length();
+        form.reader().read_to_end(&mut output).unwrap();
+        assert_eq!(output, b"");
+        assert_eq!(length.unwrap(), 0);
+    }
+
+    #[test]
+    fn read_to_end() {
+        let mut output = Vec::new();
+        let mut form = Form::new()
+            .part("reader1", Part::reader(::std::io::empty()))
+            .part("key1", Part::text("value1"))
+            .part(
+                "key2",
+                Part::text("value2").mime(::mime::IMAGE_BMP),
+            )
+            .part("reader2", Part::reader(::std::io::empty()))
+            .part(
+                "key3",
+                Part::text("value3").file_name("filename"),
+            );
+        form.boundary = "boundary".to_string();
+        let length = form.compute_length();
+        let expected = "--boundary\r\n\
+                        Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
+                        \r\n\
+                        --boundary\r\n\
+                        Content-Disposition: form-data; name=\"key1\"\r\n\r\n\
+                        value1\r\n\
+                        --boundary\r\n\
+                        Content-Disposition: form-data; name=\"key2\"\r\n\
+                        Content-Type: image/bmp\r\n\r\n\
+                        value2\r\n\
+                        --boundary\r\n\
+                        Content-Disposition: form-data; name=\"reader2\"\r\n\r\n\
+                        \r\n\
+                        --boundary\r\n\
+                        Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\
+                        value3\r\n--boundary--";
+        form.reader().read_to_end(&mut output).unwrap();
+        // These prints are for debug purposes in case the test fails
+        println!(
+            "START REAL\n{}\nEND REAL",
+            ::std::str::from_utf8(&output).unwrap()
+        );
+        println!("START EXPECTED\n{}\nEND EXPECTED", expected);
+        assert_eq!(::std::str::from_utf8(&output).unwrap(), expected);
+        assert!(length.is_none());
+    }
+
+    #[test]
+    fn read_to_end_with_length() {
+        let mut output = Vec::new();
+        let mut form = Form::new()
+            .text("key1", "value1")
+            .part(
+                "key2",
+                Part::text("value2").mime(::mime::IMAGE_BMP),
+            )
+            .part(
+                "key3",
+                Part::text("value3").file_name("filename"),
+            );
+        form.boundary = "boundary".to_string();
+        let length = form.compute_length();
+        let expected = "--boundary\r\n\
+                        Content-Disposition: form-data; name=\"key1\"\r\n\r\n\
+                        value1\r\n\
+                        --boundary\r\n\
+                        Content-Disposition: form-data; name=\"key2\"\r\n\
+                        Content-Type: image/bmp\r\n\r\n\
+                        value2\r\n\
+                        --boundary\r\n\
+                        Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\
+                        value3\r\n--boundary--";
+        form.reader().read_to_end(&mut output).unwrap();
+        // These prints are for debug purposes in case the test fails
+        println!(
+            "START REAL\n{}\nEND REAL",
+            ::std::str::from_utf8(&output).unwrap()
+        );
+        println!("START EXPECTED\n{}\nEND EXPECTED", expected);
+        assert_eq!(::std::str::from_utf8(&output).unwrap(), expected);
+        assert_eq!(length.unwrap(), expected.len() as u64);
+    }
+
+    #[test]
+    fn header_percent_encoding() {
+        let name = "start%'\"\r\nßend";
+        let field = Part::text("");
+        let expected = "Content-Disposition: form-data; name*=utf-8''start%25\'%22%0D%0A%C3%9Fend";
+
+        assert_eq!(header(name, &field), expected);
+    }
+}
diff --git a/src/request.rs b/src/request.rs
index 878ee8a69..0e7d6512a 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -277,6 +277,42 @@ impl RequestBuilder {
         Ok(self)
     }
 
+    /// Sends a multipart/form-data body.
+    ///
+    /// ```
+    /// use reqwest::mime;
+    /// # use reqwest::Error;
+    ///
+    /// # fn run() -> Result<(), Box<std::error::Error>> {
+    /// let client = reqwest::Client::new()?;
+    /// let form = reqwest::multipart::Form::new()
+    ///     .text("key3", "value3")
+    ///     .file("file", "/path/to/field")?;
+    ///
+    /// let response = client.post("your url")?
+    ///     .multipart(form)
+    ///     .send()?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// See [`multipart`](multipart/) for more examples.
+    pub fn multipart(&mut self, mut multipart: ::multipart::Form) -> &mut RequestBuilder {
+        {
+            let req = self.request_mut();
+            req.headers_mut().set(
+                ::header::ContentType(format!("multipart/form-data; boundary={}", ::multipart_::boundary(&multipart))
+                    .parse().unwrap()
+                )
+            );
+            *req.body_mut() = Some(match ::multipart_::compute_length(&mut multipart) {
+                Some(length) => Body::sized(::multipart_::reader(multipart), length),
+                None => Body::new(::multipart_::reader(multipart)),
+            })
+        }
+        self
+    }
+
     /// Build a `Request`, which can be inspected, modified and executed with
     /// `Client::execute()`.
     ///


Test patch:
diff --git a/tests/multipart.rs b/tests/multipart.rs
new file mode 100644
index 000000000..6f53d031c
--- /dev/null
+++ b/tests/multipart.rs
@@ -0,0 +1,50 @@
+extern crate reqwest;
+
+#[macro_use]
+mod support;
+
+#[test]
+fn test_multipart() {
+    let form = reqwest::multipart::Form::new()
+        .text("foo", "bar");
+
+    let expected_body = format!("\
+        --{0}\r\n\
+        Content-Disposition: form-data; name=\"foo\"\r\n\r\n\
+        bar\r\n\
+        --{0}--\
+    ", form.boundary());
+
+    let server = server! {
+        request: format!("\
+            POST /multipart/1 HTTP/1.1\r\n\
+            Host: $HOST\r\n\
+            Content-Type: multipart/form-data; boundary={}\r\n\
+            Content-Length: 123\r\n\
+            User-Agent: $USERAGENT\r\n\
+            Accept: */*\r\n\
+            Accept-Encoding: gzip\r\n\
+            \r\n\
+            {}\
+            ", form.boundary(), expected_body),
+        response: b"\
+            HTTP/1.1 200 OK\r\n\
+            Server: multipart\r\n\
+            Content-Length: 0\r\n\
+            \r\n\
+            "
+    };
+
+    let url = format!("http://{}/multipart/1", server.addr());
+
+    let res = reqwest::Client::new()
+        .unwrap()
+        .post(&url)
+        .unwrap()
+        .multipart(form)
+        .send()
+        .unwrap();
+
+    assert_eq!(res.url().as_str(), &url);
+    assert_eq!(res.status(), reqwest::StatusCode::Ok);
+}