Skip to main content

slumber_reqwest/blocking/
multipart.rs

1//! multipart/form-data
2//!
3//! To send a `multipart/form-data` body, a [`Form`] is built up, adding
4//! fields or customized [`Part`]s, and then calling the
5//! [`multipart`][builder] method on the `RequestBuilder`.
6//!
7//! # Example
8//!
9//! ```
10//! use reqwest::blocking::multipart;
11//!
12//! # fn run() -> Result<(), Box<dyn std::error::Error>> {
13//! let form = multipart::Form::new()
14//!     // Adding just a simple text field...
15//!     .text("username", "seanmonstar")
16//!     // And a file...
17//!     .file("photo", "/path/to/photo.png")?;
18//!
19//! // Customize all the details of a Part if needed...
20//! let bio = multipart::Part::text("hallo peeps")
21//!     .file_name("bio.txt")
22//!     .mime_str("text/plain")?;
23//!
24//! // Add the custom part to our form...
25//! let form = form.part("biography", bio);
26//!
27//! // And finally, send the form
28//! let client = reqwest::blocking::Client::new();
29//! let resp = client
30//!     .post("http://localhost:8080/user")
31//!     .multipart(form)
32//!     .send()?;
33//! # Ok(())
34//! # }
35//! # fn main() {}
36//! ```
37//!
38//! [builder]: ../struct.RequestBuilder.html#method.multipart
39use std::borrow::Cow;
40use std::fmt;
41use std::fs::File;
42use std::io::{self, Cursor, Read};
43use std::path::Path;
44
45use mime_guess::{self, Mime};
46
47use super::Body;
48use crate::async_impl::multipart::{FormParts, PartMetadata, PartProps};
49use crate::header::HeaderMap;
50
51/// A multipart/form-data request.
52pub struct Form {
53    inner: FormParts<Part>,
54}
55
56/// A field in a multipart form.
57pub struct Part {
58    meta: PartMetadata,
59    value: Body,
60}
61
62impl Default for Form {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68impl Form {
69    /// Creates a new Form without any content.
70    pub fn new() -> Form {
71        Form {
72            inner: FormParts::new(),
73        }
74    }
75
76    /// Get the boundary that this form will use.
77    #[inline]
78    pub fn boundary(&self) -> &str {
79        self.inner.boundary()
80    }
81
82    /// Set the boundary that this form will use. By default the boundary is a
83    /// long random string to minimize the risk of the boundary appearing in
84    /// the body content.
85    ///
86    /// **Setting a custom boundary incurs significant risk of generating
87    /// corrupted bodies.** Only use this if you need it and you understand the
88    /// risk!
89    pub fn set_boundary(&mut self, boundary: impl Into<String>) {
90        self.inner.boundary = boundary.into();
91    }
92
93    /// Add a data field with supplied name and value.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// let form = reqwest::blocking::multipart::Form::new()
99    ///     .text("username", "seanmonstar")
100    ///     .text("password", "secret");
101    /// ```
102    pub fn text<T, U>(self, name: T, value: U) -> Form
103    where
104        T: Into<Cow<'static, str>>,
105        U: Into<Cow<'static, str>>,
106    {
107        self.part(name, Part::text(value))
108    }
109
110    /// Adds a file field.
111    ///
112    /// The path will be used to try to guess the filename and mime.
113    ///
114    /// # Examples
115    ///
116    /// ```no_run
117    /// # fn run() -> std::io::Result<()> {
118    /// let form = reqwest::blocking::multipart::Form::new()
119    ///     .file("key", "/path/to/file")?;
120    /// # Ok(())
121    /// # }
122    /// ```
123    ///
124    /// # Errors
125    ///
126    /// Errors when the file cannot be opened.
127    pub fn file<T, U>(self, name: T, path: U) -> io::Result<Form>
128    where
129        T: Into<Cow<'static, str>>,
130        U: AsRef<Path>,
131    {
132        Ok(self.part(name, Part::file(path)?))
133    }
134
135    /// Adds a customized Part.
136    pub fn part<T>(self, name: T, part: Part) -> Form
137    where
138        T: Into<Cow<'static, str>>,
139    {
140        self.with_inner(move |inner| inner.part(name, part))
141    }
142
143    /// Configure this `Form` to percent-encode using the `path-segment` rules.
144    pub fn percent_encode_path_segment(self) -> Form {
145        self.with_inner(|inner| inner.percent_encode_path_segment())
146    }
147
148    /// Configure this `Form` to percent-encode using the `attr-char` rules.
149    pub fn percent_encode_attr_chars(self) -> Form {
150        self.with_inner(|inner| inner.percent_encode_attr_chars())
151    }
152
153    /// Configure this `Form` to skip percent-encoding
154    pub fn percent_encode_noop(self) -> Form {
155        self.with_inner(|inner| inner.percent_encode_noop())
156    }
157
158    pub(crate) fn reader(self) -> Reader {
159        Reader::new(self)
160    }
161
162    /// Produce a reader over the multipart form data.
163    pub fn into_reader(self) -> impl Read {
164        self.reader()
165    }
166
167    // If predictable, computes the length the request will have
168    // The length should be predictable if only String and file fields have been added,
169    // but not if a generic reader has been added;
170    pub(crate) fn compute_length(&mut self) -> Option<u64> {
171        self.inner.compute_length()
172    }
173
174    fn with_inner<F>(self, func: F) -> Self
175    where
176        F: FnOnce(FormParts<Part>) -> FormParts<Part>,
177    {
178        Form {
179            inner: func(self.inner),
180        }
181    }
182}
183
184impl fmt::Debug for Form {
185    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
186        self.inner.fmt_fields("Form", f)
187    }
188}
189
190impl Part {
191    /// Makes a text parameter.
192    pub fn text<T>(value: T) -> Part
193    where
194        T: Into<Cow<'static, str>>,
195    {
196        let body = match value.into() {
197            Cow::Borrowed(slice) => Body::from(slice),
198            Cow::Owned(string) => Body::from(string),
199        };
200        Part::new(body)
201    }
202
203    /// Makes a new parameter from arbitrary bytes.
204    pub fn bytes<T>(value: T) -> Part
205    where
206        T: Into<Cow<'static, [u8]>>,
207    {
208        let body = match value.into() {
209            Cow::Borrowed(slice) => Body::from(slice),
210            Cow::Owned(vec) => Body::from(vec),
211        };
212        Part::new(body)
213    }
214
215    /// Adds a generic reader.
216    ///
217    /// Does not set filename or mime.
218    pub fn reader<T: Read + Send + 'static>(value: T) -> Part {
219        Part::new(Body::new(value))
220    }
221
222    /// Adds a generic reader with known length.
223    ///
224    /// Does not set filename or mime.
225    pub fn reader_with_length<T: Read + Send + 'static>(value: T, length: u64) -> Part {
226        Part::new(Body::sized(value, length))
227    }
228
229    /// Makes a file parameter.
230    ///
231    /// # Errors
232    ///
233    /// Errors when the file cannot be opened.
234    pub fn file<T: AsRef<Path>>(path: T) -> io::Result<Part> {
235        let path = path.as_ref();
236        let file_name = path
237            .file_name()
238            .map(|filename| filename.to_string_lossy().into_owned());
239        let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
240        let mime = mime_guess::from_ext(ext).first_or_octet_stream();
241        let file = File::open(path)?;
242        let field = Part::new(Body::from(file)).mime(mime);
243
244        Ok(if let Some(file_name) = file_name {
245            field.file_name(file_name)
246        } else {
247            field
248        })
249    }
250
251    fn new(value: Body) -> Part {
252        Part {
253            meta: PartMetadata::new(),
254            value,
255        }
256    }
257
258    /// Tries to set the mime of this part.
259    pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
260        Ok(self.mime(mime.parse().map_err(crate::error::builder)?))
261    }
262
263    // Re-export when mime 0.4 is available, with split MediaType/MediaRange.
264    fn mime(self, mime: Mime) -> Part {
265        self.with_inner(move |inner| inner.mime(mime))
266    }
267
268    /// Sets the filename, builder style.
269    pub fn file_name<T>(self, filename: T) -> Part
270    where
271        T: Into<Cow<'static, str>>,
272    {
273        self.with_inner(move |inner| inner.file_name(filename))
274    }
275
276    /// Sets custom headers for the part.
277    pub fn headers(self, headers: HeaderMap) -> Part {
278        self.with_inner(move |inner| inner.headers(headers))
279    }
280
281    fn with_inner<F>(self, func: F) -> Self
282    where
283        F: FnOnce(PartMetadata) -> PartMetadata,
284    {
285        Part {
286            meta: func(self.meta),
287            value: self.value,
288        }
289    }
290}
291
292impl fmt::Debug for Part {
293    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
294        let mut dbg = f.debug_struct("Part");
295        dbg.field("value", &self.value);
296        self.meta.fmt_fields(&mut dbg);
297        dbg.finish()
298    }
299}
300
301impl PartProps for Part {
302    fn value_len(&self) -> Option<u64> {
303        self.value.len()
304    }
305
306    fn metadata(&self) -> &PartMetadata {
307        &self.meta
308    }
309}
310
311pub(crate) struct Reader {
312    form: Form,
313    active_reader: Option<Box<dyn Read + Send>>,
314}
315
316impl fmt::Debug for Reader {
317    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
318        f.debug_struct("Reader").field("form", &self.form).finish()
319    }
320}
321
322impl Reader {
323    fn new(form: Form) -> Reader {
324        let mut reader = Reader {
325            form,
326            active_reader: None,
327        };
328        reader.next_reader();
329        reader
330    }
331
332    fn next_reader(&mut self) {
333        self.active_reader = if !self.form.inner.fields.is_empty() {
334            // We need to move out of the vector here because we are consuming the field's reader
335            let (name, field) = self.form.inner.fields.remove(0);
336            let boundary = Cursor::new(format!("--{}\r\n", self.form.boundary()));
337            let header = Cursor::new({
338                // Try to use cached headers created by compute_length
339                let mut h = if !self.form.inner.computed_headers.is_empty() {
340                    self.form.inner.computed_headers.remove(0)
341                } else {
342                    self.form
343                        .inner
344                        .percent_encoding
345                        .encode_headers(&name, field.metadata())
346                };
347                h.extend_from_slice(b"\r\n\r\n");
348                h
349            });
350            let reader = boundary
351                .chain(header)
352                .chain(field.value.into_reader())
353                .chain(Cursor::new("\r\n"));
354            // According to https://tools.ietf.org/html/rfc2046#section-5.1.1
355            // the very last field has a special boundary
356            if !self.form.inner.fields.is_empty() {
357                Some(Box::new(reader))
358            } else {
359                Some(Box::new(reader.chain(Cursor::new(format!(
360                    "--{}--\r\n",
361                    self.form.boundary()
362                )))))
363            }
364        } else {
365            None
366        }
367    }
368}
369
370impl Read for Reader {
371    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
372        let mut total_bytes_read = 0usize;
373        let mut last_read_bytes;
374        loop {
375            match self.active_reader {
376                Some(ref mut reader) => {
377                    last_read_bytes = reader.read(&mut buf[total_bytes_read..])?;
378                    total_bytes_read += last_read_bytes;
379                    if total_bytes_read == buf.len() {
380                        return Ok(total_bytes_read);
381                    }
382                }
383                None => return Ok(total_bytes_read),
384            };
385            if last_read_bytes == 0 && !buf.is_empty() {
386                self.next_reader();
387            }
388        }
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    #[test]
397    fn form_empty() {
398        let mut output = Vec::new();
399        let mut form = Form::new();
400        let length = form.compute_length();
401        form.reader().read_to_end(&mut output).unwrap();
402        assert_eq!(output, b"");
403        assert_eq!(length.unwrap(), 0);
404    }
405
406    #[test]
407    fn read_to_end() {
408        let mut output = Vec::new();
409        let mut form = Form::new()
410            .part("reader1", Part::reader(std::io::empty()))
411            .part("key1", Part::text("value1"))
412            .part(
413                "key2",
414                Part::text("value2").mime(mime_guess::mime::IMAGE_BMP),
415            )
416            .part("reader2", Part::reader(std::io::empty()))
417            .part("key3", Part::text("value3").file_name("filename"));
418        form.inner.boundary = "boundary".to_string();
419        let length = form.compute_length();
420        let expected = "--boundary\r\n\
421             Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
422             \r\n\
423             --boundary\r\n\
424             Content-Disposition: form-data; name=\"key1\"\r\n\r\n\
425             value1\r\n\
426             --boundary\r\n\
427             Content-Disposition: form-data; name=\"key2\"\r\n\
428             Content-Type: image/bmp\r\n\r\n\
429             value2\r\n\
430             --boundary\r\n\
431             Content-Disposition: form-data; name=\"reader2\"\r\n\r\n\
432             \r\n\
433             --boundary\r\n\
434             Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\
435             value3\r\n--boundary--\r\n";
436        form.reader().read_to_end(&mut output).unwrap();
437        // These prints are for debug purposes in case the test fails
438        println!(
439            "START REAL\n{}\nEND REAL",
440            std::str::from_utf8(&output).unwrap()
441        );
442        println!("START EXPECTED\n{expected}\nEND EXPECTED");
443        assert_eq!(std::str::from_utf8(&output).unwrap(), expected);
444        assert!(length.is_none());
445    }
446
447    #[test]
448    fn read_to_end_with_length() {
449        let mut output = Vec::new();
450        let mut form = Form::new()
451            .text("key1", "value1")
452            .part(
453                "key2",
454                Part::text("value2").mime(mime_guess::mime::IMAGE_BMP),
455            )
456            .part("key3", Part::text("value3").file_name("filename"));
457        form.inner.boundary = "boundary".to_string();
458        let length = form.compute_length();
459        let expected = "--boundary\r\n\
460             Content-Disposition: form-data; name=\"key1\"\r\n\r\n\
461             value1\r\n\
462             --boundary\r\n\
463             Content-Disposition: form-data; name=\"key2\"\r\n\
464             Content-Type: image/bmp\r\n\r\n\
465             value2\r\n\
466             --boundary\r\n\
467             Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\
468             value3\r\n--boundary--\r\n";
469        form.reader().read_to_end(&mut output).unwrap();
470        // These prints are for debug purposes in case the test fails
471        println!(
472            "START REAL\n{}\nEND REAL",
473            std::str::from_utf8(&output).unwrap()
474        );
475        println!("START EXPECTED\n{expected}\nEND EXPECTED");
476        assert_eq!(std::str::from_utf8(&output).unwrap(), expected);
477        assert_eq!(length.unwrap(), expected.len() as u64);
478    }
479
480    #[test]
481    fn read_to_end_with_header() {
482        let mut output = Vec::new();
483        let mut part = Part::text("value2").mime(mime_guess::mime::IMAGE_BMP);
484        let mut headers = HeaderMap::new();
485        headers.insert("Hdr3", "/a/b/c".parse().unwrap());
486        part = part.headers(headers);
487        let mut form = Form::new().part("key2", part);
488        form.inner.boundary = "boundary".to_string();
489        let expected = "--boundary\r\n\
490                        Content-Disposition: form-data; name=\"key2\"\r\n\
491                        Content-Type: image/bmp\r\n\
492                        hdr3: /a/b/c\r\n\
493                        \r\n\
494                        value2\r\n\
495                        --boundary--\r\n";
496        form.reader().read_to_end(&mut output).unwrap();
497        // These prints are for debug purposes in case the test fails
498        println!(
499            "START REAL\n{}\nEND REAL",
500            std::str::from_utf8(&output).unwrap()
501        );
502        println!("START EXPECTED\n{expected}\nEND EXPECTED");
503        assert_eq!(std::str::from_utf8(&output).unwrap(), expected);
504    }
505}