Skip to main content

slumber_reqwest/async_impl/
multipart.rs

1//! multipart/form-data
2use std::borrow::Cow;
3use std::fmt;
4use std::pin::Pin;
5
6#[cfg(feature = "stream")]
7use std::io;
8#[cfg(feature = "stream")]
9use std::path::Path;
10
11use bytes::Bytes;
12use mime_guess::Mime;
13use percent_encoding::{self, AsciiSet, NON_ALPHANUMERIC};
14#[cfg(feature = "stream")]
15use tokio::fs::File;
16
17use futures_core::Stream;
18use futures_util::{future, stream, StreamExt};
19use http_body_util::BodyExt;
20
21use super::Body;
22use crate::header::HeaderMap;
23
24/// An async multipart/form-data request.
25pub struct Form {
26    inner: FormParts<Part>,
27}
28
29/// A field in a multipart form.
30pub struct Part {
31    meta: PartMetadata,
32    value: Body,
33    body_length: Option<u64>,
34}
35
36pub(crate) struct FormParts<P> {
37    pub(crate) boundary: String,
38    pub(crate) computed_headers: Vec<Vec<u8>>,
39    pub(crate) fields: Vec<(Cow<'static, str>, P)>,
40    pub(crate) percent_encoding: PercentEncoding,
41}
42
43pub(crate) struct PartMetadata {
44    mime: Option<Mime>,
45    file_name: Option<Cow<'static, str>>,
46    pub(crate) headers: HeaderMap,
47}
48
49pub(crate) trait PartProps {
50    fn value_len(&self) -> Option<u64>;
51    fn metadata(&self) -> &PartMetadata;
52}
53
54// ===== impl Form =====
55
56impl Default for Form {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl Form {
63    /// Creates a new async Form without any content.
64    pub fn new() -> Form {
65        Form {
66            inner: FormParts::new(),
67        }
68    }
69
70    /// Get the boundary that this form will use.
71    #[inline]
72    pub fn boundary(&self) -> &str {
73        self.inner.boundary()
74    }
75
76    /// Set the boundary that this form will use. By default the boundary is a
77    /// long random string to minimize the risk of the boundary appearing in
78    /// the body content.
79    ///
80    /// **Setting a custom boundary incurs significant risk of generating
81    /// corrupted bodies.** Only use this if you need it and you understand the
82    /// risk!
83    pub fn set_boundary(&mut self, boundary: impl Into<String>) {
84        self.inner.boundary = boundary.into();
85    }
86
87    /// Add a data field with supplied name and value.
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// let form = reqwest::multipart::Form::new()
93    ///     .text("username", "seanmonstar")
94    ///     .text("password", "secret");
95    /// ```
96    pub fn text<T, U>(self, name: T, value: U) -> Form
97    where
98        T: Into<Cow<'static, str>>,
99        U: Into<Cow<'static, str>>,
100    {
101        self.part(name, Part::text(value))
102    }
103
104    /// Adds a file field.
105    ///
106    /// The path will be used to try to guess the filename and mime.
107    ///
108    /// # Examples
109    ///
110    /// ```no_run
111    /// # async fn run() -> std::io::Result<()> {
112    /// let form = reqwest::multipart::Form::new()
113    ///     .file("key", "/path/to/file").await?;
114    /// # Ok(())
115    /// # }
116    /// ```
117    ///
118    /// # Errors
119    ///
120    /// Errors when the file cannot be opened.
121    #[cfg(feature = "stream")]
122    #[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
123    pub async fn file<T, U>(self, name: T, path: U) -> io::Result<Form>
124    where
125        T: Into<Cow<'static, str>>,
126        U: AsRef<Path>,
127    {
128        Ok(self.part(name, Part::file(path).await?))
129    }
130
131    /// Adds a customized Part.
132    pub fn part<T>(self, name: T, part: Part) -> Form
133    where
134        T: Into<Cow<'static, str>>,
135    {
136        self.with_inner(move |inner| inner.part(name, part))
137    }
138
139    /// Configure this `Form` to percent-encode using the `path-segment` rules.
140    pub fn percent_encode_path_segment(self) -> Form {
141        self.with_inner(|inner| inner.percent_encode_path_segment())
142    }
143
144    /// Configure this `Form` to percent-encode using the `attr-char` rules.
145    pub fn percent_encode_attr_chars(self) -> Form {
146        self.with_inner(|inner| inner.percent_encode_attr_chars())
147    }
148
149    /// Configure this `Form` to skip percent-encoding
150    pub fn percent_encode_noop(self) -> Form {
151        self.with_inner(|inner| inner.percent_encode_noop())
152    }
153
154    /// Consume this instance and transform into an instance of Body for use in a request.
155    pub(crate) fn stream(self) -> Body {
156        if self.inner.fields.is_empty() {
157            return Body::empty();
158        }
159
160        Body::stream(self.into_stream())
161    }
162
163    /// Produce a stream of the bytes in this `Form`, consuming it.
164    pub fn into_stream(mut self) -> impl Stream<Item = Result<Bytes, crate::Error>> + Send + Sync {
165        if self.inner.fields.is_empty() {
166            let empty_stream: Pin<
167                Box<dyn Stream<Item = Result<Bytes, crate::Error>> + Send + Sync>,
168            > = Box::pin(futures_util::stream::empty());
169            return empty_stream;
170        }
171
172        // create initial part to init reduce chain
173        let (name, part) = self.inner.fields.remove(0);
174        let start = Box::pin(self.part_stream(name, part))
175            as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>;
176
177        let fields = self.inner.take_fields();
178        // for each field, chain an additional stream
179        let stream = fields.into_iter().fold(start, |memo, (name, part)| {
180            let part_stream = self.part_stream(name, part);
181            Box::pin(memo.chain(part_stream))
182                as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>
183        });
184        // append special ending boundary
185        let last = stream::once(future::ready(Ok(
186            format!("--{}--\r\n", self.boundary()).into()
187        )));
188        Box::pin(stream.chain(last))
189    }
190
191    /// Generate a hyper::Body stream for a single Part instance of a Form request.
192    pub(crate) fn part_stream<T>(
193        &mut self,
194        name: T,
195        part: Part,
196    ) -> impl Stream<Item = Result<Bytes, crate::Error>>
197    where
198        T: Into<Cow<'static, str>>,
199    {
200        // start with boundary
201        let boundary = stream::once(future::ready(Ok(
202            format!("--{}\r\n", self.boundary()).into()
203        )));
204        // append headers
205        let header = stream::once(future::ready(Ok({
206            let mut h = self
207                .inner
208                .percent_encoding
209                .encode_headers(&name.into(), &part.meta);
210            h.extend_from_slice(b"\r\n\r\n");
211            h.into()
212        })));
213        // then append form data followed by terminating CRLF
214        boundary
215            .chain(header)
216            .chain(part.value.into_data_stream())
217            .chain(stream::once(future::ready(Ok("\r\n".into()))))
218    }
219
220    pub(crate) fn compute_length(&mut self) -> Option<u64> {
221        self.inner.compute_length()
222    }
223
224    fn with_inner<F>(self, func: F) -> Self
225    where
226        F: FnOnce(FormParts<Part>) -> FormParts<Part>,
227    {
228        Form {
229            inner: func(self.inner),
230        }
231    }
232}
233
234impl fmt::Debug for Form {
235    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
236        self.inner.fmt_fields("Form", f)
237    }
238}
239
240// ===== impl Part =====
241
242impl Part {
243    /// Makes a text parameter.
244    pub fn text<T>(value: T) -> Part
245    where
246        T: Into<Cow<'static, str>>,
247    {
248        let body = match value.into() {
249            Cow::Borrowed(slice) => Body::from(slice),
250            Cow::Owned(string) => Body::from(string),
251        };
252        Part::new(body, None)
253    }
254
255    /// Makes a new parameter from arbitrary bytes.
256    pub fn bytes<T>(value: T) -> Part
257    where
258        T: Into<Cow<'static, [u8]>>,
259    {
260        let body = match value.into() {
261            Cow::Borrowed(slice) => Body::from(slice),
262            Cow::Owned(vec) => Body::from(vec),
263        };
264        Part::new(body, None)
265    }
266
267    /// Makes a new parameter from an arbitrary stream.
268    pub fn stream<T: Into<Body>>(value: T) -> Part {
269        Part::new(value.into(), None)
270    }
271
272    /// Makes a new parameter from an arbitrary stream with a known length. This is particularly
273    /// useful when adding something like file contents as a stream, where you can know the content
274    /// length beforehand.
275    pub fn stream_with_length<T: Into<Body>>(value: T, length: u64) -> Part {
276        Part::new(value.into(), Some(length))
277    }
278
279    /// Makes a file parameter.
280    ///
281    /// # Errors
282    ///
283    /// Errors when the file cannot be opened.
284    #[cfg(feature = "stream")]
285    #[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
286    pub async fn file<T: AsRef<Path>>(path: T) -> io::Result<Part> {
287        let path = path.as_ref();
288        let file_name = path
289            .file_name()
290            .map(|filename| filename.to_string_lossy().into_owned());
291        let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
292        let mime = mime_guess::from_ext(ext).first_or_octet_stream();
293        let file = File::open(path).await?;
294        let len = file.metadata().await.map(|m| m.len()).ok();
295        let field = match len {
296            Some(len) => Part::stream_with_length(file, len),
297            None => Part::stream(file),
298        }
299        .mime(mime);
300
301        Ok(if let Some(file_name) = file_name {
302            field.file_name(file_name)
303        } else {
304            field
305        })
306    }
307
308    fn new(value: Body, body_length: Option<u64>) -> Part {
309        Part {
310            meta: PartMetadata::new(),
311            value,
312            body_length,
313        }
314    }
315
316    /// Tries to set the mime of this part.
317    pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
318        Ok(self.mime(mime.parse().map_err(crate::error::builder)?))
319    }
320
321    // Re-export when mime 0.4 is available, with split MediaType/MediaRange.
322    fn mime(self, mime: Mime) -> Part {
323        self.with_inner(move |inner| inner.mime(mime))
324    }
325
326    /// Sets the filename, builder style.
327    pub fn file_name<T>(self, filename: T) -> Part
328    where
329        T: Into<Cow<'static, str>>,
330    {
331        self.with_inner(move |inner| inner.file_name(filename))
332    }
333
334    /// Sets custom headers for the part.
335    pub fn headers(self, headers: HeaderMap) -> Part {
336        self.with_inner(move |inner| inner.headers(headers))
337    }
338
339    fn with_inner<F>(self, func: F) -> Self
340    where
341        F: FnOnce(PartMetadata) -> PartMetadata,
342    {
343        Part {
344            meta: func(self.meta),
345            ..self
346        }
347    }
348}
349
350impl fmt::Debug for Part {
351    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
352        let mut dbg = f.debug_struct("Part");
353        dbg.field("value", &self.value);
354        self.meta.fmt_fields(&mut dbg);
355        dbg.finish()
356    }
357}
358
359impl PartProps for Part {
360    fn value_len(&self) -> Option<u64> {
361        if self.body_length.is_some() {
362            self.body_length
363        } else {
364            self.value.content_length()
365        }
366    }
367
368    fn metadata(&self) -> &PartMetadata {
369        &self.meta
370    }
371}
372
373// ===== impl FormParts =====
374
375impl<P: PartProps> FormParts<P> {
376    pub(crate) fn new() -> Self {
377        FormParts {
378            boundary: gen_boundary(),
379            computed_headers: Vec::new(),
380            fields: Vec::new(),
381            percent_encoding: PercentEncoding::PathSegment,
382        }
383    }
384
385    pub(crate) fn boundary(&self) -> &str {
386        &self.boundary
387    }
388
389    /// Adds a customized Part.
390    pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
391    where
392        T: Into<Cow<'static, str>>,
393    {
394        self.fields.push((name.into(), part));
395        self
396    }
397
398    /// Configure this `Form` to percent-encode using the `path-segment` rules.
399    pub(crate) fn percent_encode_path_segment(mut self) -> Self {
400        self.percent_encoding = PercentEncoding::PathSegment;
401        self
402    }
403
404    /// Configure this `Form` to percent-encode using the `attr-char` rules.
405    pub(crate) fn percent_encode_attr_chars(mut self) -> Self {
406        self.percent_encoding = PercentEncoding::AttrChar;
407        self
408    }
409
410    /// Configure this `Form` to skip percent-encoding
411    pub(crate) fn percent_encode_noop(mut self) -> Self {
412        self.percent_encoding = PercentEncoding::NoOp;
413        self
414    }
415
416    // If predictable, computes the length the request will have
417    // The length should be predictable if only String and file fields have been added,
418    // but not if a generic reader has been added;
419    pub(crate) fn compute_length(&mut self) -> Option<u64> {
420        let mut length = 0u64;
421        for &(ref name, ref field) in self.fields.iter() {
422            match field.value_len() {
423                Some(value_length) => {
424                    // We are constructing the header just to get its length. To not have to
425                    // construct it again when the request is sent we cache these headers.
426                    let header = self.percent_encoding.encode_headers(name, field.metadata());
427                    let header_length = header.len();
428                    self.computed_headers.push(header);
429                    // The additions mimic the format string out of which the field is constructed
430                    // in Reader. Not the cleanest solution because if that format string is
431                    // ever changed then this formula needs to be changed too which is not an
432                    // obvious dependency in the code.
433                    length += 2
434                        + self.boundary().len() as u64
435                        + 2
436                        + header_length as u64
437                        + 4
438                        + value_length
439                        + 2
440                }
441                _ => return None,
442            }
443        }
444        // If there is at least one field there is a special boundary for the very last field.
445        if !self.fields.is_empty() {
446            length += 2 + self.boundary().len() as u64 + 4
447        }
448        Some(length)
449    }
450
451    /// Take the fields vector of this instance, replacing with an empty vector.
452    fn take_fields(&mut self) -> Vec<(Cow<'static, str>, P)> {
453        std::mem::replace(&mut self.fields, Vec::new())
454    }
455}
456
457impl<P: fmt::Debug> FormParts<P> {
458    pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
459        f.debug_struct(ty_name)
460            .field("boundary", &self.boundary)
461            .field("parts", &self.fields)
462            .finish()
463    }
464}
465
466// ===== impl PartMetadata =====
467
468impl PartMetadata {
469    pub(crate) fn new() -> Self {
470        PartMetadata {
471            mime: None,
472            file_name: None,
473            headers: HeaderMap::default(),
474        }
475    }
476
477    pub(crate) fn mime(mut self, mime: Mime) -> Self {
478        self.mime = Some(mime);
479        self
480    }
481
482    pub(crate) fn file_name<T>(mut self, filename: T) -> Self
483    where
484        T: Into<Cow<'static, str>>,
485    {
486        self.file_name = Some(filename.into());
487        self
488    }
489
490    pub(crate) fn headers<T>(mut self, headers: T) -> Self
491    where
492        T: Into<HeaderMap>,
493    {
494        self.headers = headers.into();
495        self
496    }
497}
498
499impl PartMetadata {
500    pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
501        &self,
502        debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>,
503    ) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
504        debug_struct
505            .field("mime", &self.mime)
506            .field("file_name", &self.file_name)
507            .field("headers", &self.headers)
508    }
509}
510
511// https://url.spec.whatwg.org/#fragment-percent-encode-set
512const FRAGMENT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS
513    .add(b' ')
514    .add(b'"')
515    .add(b'<')
516    .add(b'>')
517    .add(b'`');
518
519// https://url.spec.whatwg.org/#path-percent-encode-set
520const PATH_ENCODE_SET: &AsciiSet = &FRAGMENT_ENCODE_SET.add(b'#').add(b'?').add(b'{').add(b'}');
521
522const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &PATH_ENCODE_SET.add(b'/').add(b'%');
523
524// https://tools.ietf.org/html/rfc8187#section-3.2.1
525const ATTR_CHAR_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
526    .remove(b'!')
527    .remove(b'#')
528    .remove(b'$')
529    .remove(b'&')
530    .remove(b'+')
531    .remove(b'-')
532    .remove(b'.')
533    .remove(b'^')
534    .remove(b'_')
535    .remove(b'`')
536    .remove(b'|')
537    .remove(b'~');
538
539pub(crate) enum PercentEncoding {
540    PathSegment,
541    AttrChar,
542    NoOp,
543}
544
545impl PercentEncoding {
546    pub(crate) fn encode_headers(&self, name: &str, field: &PartMetadata) -> Vec<u8> {
547        let mut buf = Vec::new();
548        buf.extend_from_slice(b"Content-Disposition: form-data; ");
549
550        match self.percent_encode(name) {
551            Cow::Borrowed(value) => {
552                // nothing has been percent encoded
553                buf.extend_from_slice(b"name=\"");
554                buf.extend_from_slice(value.as_bytes());
555                buf.extend_from_slice(b"\"");
556            }
557            Cow::Owned(value) => {
558                // something has been percent encoded
559                buf.extend_from_slice(b"name*=utf-8''");
560                buf.extend_from_slice(value.as_bytes());
561            }
562        }
563
564        // According to RFC7578 Section 4.2, `filename*=` syntax is invalid.
565        // See https://github.com/seanmonstar/reqwest/issues/419.
566        if let Some(filename) = &field.file_name {
567            buf.extend_from_slice(b"; filename=\"");
568            let legal_filename = filename
569                .replace('\\', "\\\\")
570                .replace('"', "\\\"")
571                .replace('\r', "\\\r")
572                .replace('\n', "\\\n");
573            buf.extend_from_slice(legal_filename.as_bytes());
574            buf.extend_from_slice(b"\"");
575        }
576
577        if let Some(mime) = &field.mime {
578            buf.extend_from_slice(b"\r\nContent-Type: ");
579            buf.extend_from_slice(mime.as_ref().as_bytes());
580        }
581
582        for (k, v) in field.headers.iter() {
583            buf.extend_from_slice(b"\r\n");
584            buf.extend_from_slice(k.as_str().as_bytes());
585            buf.extend_from_slice(b": ");
586            buf.extend_from_slice(v.as_bytes());
587        }
588        buf
589    }
590
591    fn percent_encode<'a>(&self, value: &'a str) -> Cow<'a, str> {
592        use percent_encoding::utf8_percent_encode as percent_encode;
593
594        match self {
595            Self::PathSegment => percent_encode(value, PATH_SEGMENT_ENCODE_SET).into(),
596            Self::AttrChar => percent_encode(value, ATTR_CHAR_ENCODE_SET).into(),
597            Self::NoOp => value.into(),
598        }
599    }
600}
601
602fn gen_boundary() -> String {
603    use crate::util::fast_random as random;
604
605    let a = random();
606    let b = random();
607    let c = random();
608    let d = random();
609
610    format!("{a:016x}-{b:016x}-{c:016x}-{d:016x}")
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616    use futures_util::stream;
617    use futures_util::TryStreamExt;
618    use std::future;
619    use tokio::{self, runtime};
620
621    #[test]
622    fn form_empty() {
623        let form = Form::new();
624
625        let rt = runtime::Builder::new_current_thread()
626            .enable_all()
627            .build()
628            .expect("new rt");
629        let body = form.stream().into_data_stream();
630        let s = body.map_ok(|try_c| try_c.to_vec()).try_concat();
631
632        let out = rt.block_on(s);
633        assert!(out.unwrap().is_empty());
634    }
635
636    #[test]
637    fn stream_to_end() {
638        let mut form = Form::new()
639            .part(
640                "reader1",
641                Part::stream(Body::stream(stream::once(future::ready::<
642                    Result<String, crate::Error>,
643                >(Ok(
644                    "part1".to_owned()
645                ))))),
646            )
647            .part("key1", Part::text("value1"))
648            .part(
649                "key2",
650                Part::text("value2").mime(mime_guess::mime::IMAGE_BMP),
651            )
652            .part(
653                "reader2",
654                Part::stream(Body::stream(stream::once(future::ready::<
655                    Result<String, crate::Error>,
656                >(Ok(
657                    "part2".to_owned()
658                ))))),
659            )
660            .part("key3", Part::text("value3").file_name("filename"));
661        form.inner.boundary = "boundary".to_string();
662        let expected = "--boundary\r\n\
663             Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
664             part1\r\n\
665             --boundary\r\n\
666             Content-Disposition: form-data; name=\"key1\"\r\n\r\n\
667             value1\r\n\
668             --boundary\r\n\
669             Content-Disposition: form-data; name=\"key2\"\r\n\
670             Content-Type: image/bmp\r\n\r\n\
671             value2\r\n\
672             --boundary\r\n\
673             Content-Disposition: form-data; name=\"reader2\"\r\n\r\n\
674             part2\r\n\
675             --boundary\r\n\
676             Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\
677             value3\r\n--boundary--\r\n";
678        let rt = runtime::Builder::new_current_thread()
679            .enable_all()
680            .build()
681            .expect("new rt");
682        let body = form.stream().into_data_stream();
683        let s = body.map(|try_c| try_c.map(|r| r.to_vec())).try_concat();
684
685        let out = rt.block_on(s).unwrap();
686        // These prints are for debug purposes in case the test fails
687        println!(
688            "START REAL\n{}\nEND REAL",
689            std::str::from_utf8(&out).unwrap()
690        );
691        println!("START EXPECTED\n{expected}\nEND EXPECTED");
692        assert_eq!(std::str::from_utf8(&out).unwrap(), expected);
693    }
694
695    #[test]
696    fn stream_to_end_with_header() {
697        let mut part = Part::text("value2").mime(mime_guess::mime::IMAGE_BMP);
698        let mut headers = HeaderMap::new();
699        headers.insert("Hdr3", "/a/b/c".parse().unwrap());
700        part = part.headers(headers);
701        let mut form = Form::new().part("key2", part);
702        form.inner.boundary = "boundary".to_string();
703        let expected = "--boundary\r\n\
704                        Content-Disposition: form-data; name=\"key2\"\r\n\
705                        Content-Type: image/bmp\r\n\
706                        hdr3: /a/b/c\r\n\
707                        \r\n\
708                        value2\r\n\
709                        --boundary--\r\n";
710        let rt = runtime::Builder::new_current_thread()
711            .enable_all()
712            .build()
713            .expect("new rt");
714        let body = form.stream().into_data_stream();
715        let s = body.map(|try_c| try_c.map(|r| r.to_vec())).try_concat();
716
717        let out = rt.block_on(s).unwrap();
718        // These prints are for debug purposes in case the test fails
719        println!(
720            "START REAL\n{}\nEND REAL",
721            std::str::from_utf8(&out).unwrap()
722        );
723        println!("START EXPECTED\n{expected}\nEND EXPECTED");
724        assert_eq!(std::str::from_utf8(&out).unwrap(), expected);
725    }
726
727    #[test]
728    fn correct_content_length() {
729        // Setup an arbitrary data stream
730        let stream_data = b"just some stream data";
731        let stream_len = stream_data.len();
732        let stream_data = stream_data
733            .chunks(3)
734            .map(|c| Ok::<_, std::io::Error>(Bytes::from(c)));
735        let the_stream = futures_util::stream::iter(stream_data);
736
737        let bytes_data = b"some bytes data".to_vec();
738        let bytes_len = bytes_data.len();
739
740        let stream_part = Part::stream_with_length(Body::stream(the_stream), stream_len as u64);
741        let body_part = Part::bytes(bytes_data);
742
743        // A simple check to make sure we get the configured body length
744        assert_eq!(stream_part.value_len().unwrap(), stream_len as u64);
745
746        // Make sure it delegates to the underlying body if length is not specified
747        assert_eq!(body_part.value_len().unwrap(), bytes_len as u64);
748    }
749
750    #[test]
751    fn header_percent_encoding() {
752        let name = "start%'\"\r\nßend";
753        let field = Part::text("");
754
755        assert_eq!(
756            PercentEncoding::PathSegment.encode_headers(name, &field.meta),
757            &b"Content-Disposition: form-data; name*=utf-8''start%25'%22%0D%0A%C3%9Fend"[..]
758        );
759
760        assert_eq!(
761            PercentEncoding::AttrChar.encode_headers(name, &field.meta),
762            &b"Content-Disposition: form-data; name*=utf-8''start%25%27%22%0D%0A%C3%9Fend"[..]
763        );
764    }
765}