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