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