Skip to main content

blitz_net/
lib.rs

1//! Networking (HTTP, filesystem, Data URIs) for Blitz
2//!
3//! Provides implementations of [`blitz_traits::net::NetProvider`], which loads
4//! the resources a document needs, and of
5//! [`blitz_traits::platform::FetchProvider`], which serves a guest's `fetch()`.
6//!
7//! Both use one native HTTP client, sharing its connection pools, cookie jar,
8//! compression codecs and private cache. HTTPS prefers HTTP/2 through ALPN,
9//! with one multiplexed connection per origin; HTTP/1.1 serves servers that
10//! do not offer h2, with one kept-alive connection per origin.
11//!
12//! Creating a native provider starts nagoya's process background runtime and
13//! the house client's shared reactor if absent. Multiple providers reuse them.
14//! Embedders do not need to enter an executor: callback requests use
15//! `nagoya::spawn`, and async methods also work under `nagoya::block_on`.
16//!
17//! Cookie persistence belongs to the embedder's cookie jar. Disk caching is
18//! attached explicitly with a profile directory; no global cache path is used.
19//! On wasm32, local URL handling remains available, but this native socket
20//! transport does not provide HTTP.
21
22#[cfg(not(target_arch = "wasm32"))]
23mod h1;
24
25use blitz_traits::net::{
26    AbortSignal, Body, Bytes, CookieJar as CookieStorage, NetHandler, NetProvider, NetWaker,
27    Request,
28    http::{HeaderValue, header::CONTENT_TYPE},
29};
30use blitz_traits::platform::{
31    FetchError, FetchHandler, FetchProvider, FetchRequest, FetchResponse, HeaderMap, StatusCode,
32};
33use data_url::DataUrl;
34use std::{marker::PhantomData, pin::Pin, sync::Arc, task::Poll};
35
36/// Cookie storage supplied to [`Provider::with_user_agent_and_cookie_provider`].
37///
38/// This house trait keeps embedders independent of the HTTP client. The provider
39/// consults the jar for each request and stores cookies from every response,
40/// including intermediate redirects and error responses.
41#[cfg(feature = "cookies")]
42pub use blitz_traits::net::CookieJar;
43
44#[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
45pub use h1::cache::PrivateCache;
46
47/// Whether the latest TLS connection to a host needed the RSA fallback.
48#[cfg(not(target_arch = "wasm32"))]
49pub use h1::rsa_fallback_state;
50
51/// Per-host connection counters, for logs and inspection.
52#[cfg(not(target_arch = "wasm32"))]
53pub use h1::{HostStats, connection_stats};
54
55/// The `User-Agent` sent when a caller does not choose one.
56///
57/// Kept as it was so that existing consumers see no change in what servers
58/// send them. It is worth knowing that this string is a 2020 Firefox on Linux,
59/// and that a large share of the popular web serves a degraded page, a
60/// challenge stub or nothing at all to a client it does not recognise. A
61/// consumer that wants the page a current browser would get has to say so with
62/// [`Provider::with_user_agent`].
63pub const DEFAULT_USER_AGENT: &str =
64    "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0";
65
66#[cfg(not(target_arch = "wasm32"))]
67type Client = h1::H1Client;
68
69#[cfg(target_arch = "wasm32")]
70#[derive(Clone)]
71struct Client;
72
73#[cfg(target_arch = "wasm32")]
74impl Client {
75    fn new(user_agent: &str, _cookies: Option<Arc<dyn CookieStorage>>) -> Result<Self, FetchError> {
76        HeaderValue::from_str(user_agent)
77            .map_err(|error| FetchError::InvalidRequest(error.to_string()))?;
78        Ok(Self)
79    }
80
81    async fn fetch(&self, _request: FetchRequest) -> Result<FetchResponse, FetchError> {
82        Err(FetchError::Network(
83            "the native HTTP transport is unavailable on wasm32".to_owned(),
84        ))
85    }
86}
87
88#[cfg(target_arch = "wasm32")]
89fn spawn(fut: impl Future + 'static) {
90    wasm_bindgen_futures::spawn_local(async move {
91        fut.await;
92    });
93}
94
95#[cfg(not(target_arch = "wasm32"))]
96fn spawn<F>(fut: F)
97where
98    F: Future + Send + 'static,
99    F::Output: Send + 'static,
100{
101    nagoya::spawn(fut);
102}
103
104/// Document and script networking over one house client.
105///
106/// Native construction initializes the shared runtime and reactor. Supply a
107/// cookie jar for cookie storage and attach a profile cache before sharing the
108/// provider with documents.
109pub struct Provider {
110    client: Client,
111    waker: Arc<dyn NetWaker>,
112    user_agent: Arc<str>,
113    #[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
114    cache_manager: Option<Arc<PrivateCache>>,
115}
116impl Provider {
117    pub fn new(waker: Option<Arc<dyn NetWaker>>) -> Self {
118        Self::with_user_agent(waker, DEFAULT_USER_AGENT)
119    }
120
121    /// A provider that identifies itself as `user_agent`.
122    ///
123    /// The string is sent verbatim on document, subresource and script
124    /// requests alike, because a server that decides what to serve from the
125    /// `User-Agent` will do so on every one of them, and a client that agreed
126    /// with itself on only some would fetch a page assembled for two different
127    /// browsers.
128    pub fn with_user_agent(waker: Option<Arc<dyn NetWaker>>, user_agent: &str) -> Self {
129        Self::with_cookie_jar(waker, user_agent, None)
130    }
131
132    /// A provider that uses an embedder-owned cookie jar and identity.
133    ///
134    /// The jar supplies cookies for each request and receives cookies from
135    /// intermediate redirects, error responses, and final responses.
136    #[cfg(feature = "cookies")]
137    pub fn with_user_agent_and_cookie_provider<C>(
138        waker: Option<Arc<dyn NetWaker>>,
139        user_agent: &str,
140        cookie_provider: Arc<C>,
141    ) -> Self
142    where
143        C: CookieJar,
144    {
145        Self::with_cookie_jar(waker, user_agent, Some(cookie_provider))
146    }
147
148    fn with_cookie_jar(
149        waker: Option<Arc<dyn NetWaker>>,
150        user_agent: &str,
151        cookies: Option<Arc<dyn CookieStorage>>,
152    ) -> Self {
153        let client =
154            Client::new(user_agent, cookies).expect("failed to initialize the house HTTP client");
155        let waker = waker.unwrap_or(Arc::new(DummyNetWaker));
156        Self {
157            client,
158            waker,
159            user_agent: Arc::from(user_agent),
160            #[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
161            cache_manager: None,
162        }
163    }
164
165    /// Attach a cache already opened for this profile.
166    ///
167    /// Keep one open cache instance per profile. Document and script requests
168    /// share it, and cached Set-Cookie headers are never replayed.
169    #[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
170    pub fn with_cache(mut self, cache: Arc<PrivateCache>) -> Self {
171        self.client = self.client.with_cache(cache.clone());
172        self.cache_manager = Some(cache);
173        self
174    }
175
176    /// Open persisted WorkTable metadata and bodies under the profile directory.
177    ///
178    /// Opening runs on a nagoya worker. Failure leaves the caller free to build
179    /// a provider without a cache instead.
180    #[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
181    pub async fn with_cache_directory(
182        self,
183        profile_directory: impl AsRef<std::path::Path>,
184    ) -> std::io::Result<Self> {
185        let directory = profile_directory.as_ref().to_owned();
186        let cache = nagoya::spawn(async move { PrivateCache::open(directory).await })
187            .await
188            .ok_or_else(|| std::io::Error::other("cache opener was cancelled"))??;
189        Ok(self.with_cache(cache))
190    }
191
192    pub fn shared(waker: Option<Arc<dyn NetWaker>>) -> Arc<dyn NetProvider> {
193        Arc::new(Self::new(waker))
194    }
195    pub fn shared_with_user_agent(
196        waker: Option<Arc<dyn NetWaker>>,
197        user_agent: &str,
198    ) -> Arc<dyn NetProvider> {
199        Arc::new(Self::with_user_agent(waker, user_agent))
200    }
201    /// What this provider identifies itself as.
202    pub fn user_agent(&self) -> &str {
203        &self.user_agent
204    }
205    pub fn is_empty(&self) -> bool {
206        Arc::strong_count(&self.waker) == 1
207    }
208    pub fn count(&self) -> usize {
209        Arc::strong_count(&self.waker) - 1
210    }
211
212    #[cfg(all(feature = "cache", not(target_arch = "wasm32")))]
213    pub async fn clear_cache(&self) {
214        if let Some(cache) = &self.cache_manager {
215            let cache = cache.clone();
216            let result = nagoya::spawn(async move { cache.clear().await }).await;
217            if let Some(Err(e)) = result {
218                #[cfg(feature = "tracing")]
219                tracing::error!("Failed to clear HTTP cache: {:?}", e);
220                #[cfg(not(feature = "tracing"))]
221                let _ = e;
222            }
223        }
224    }
225}
226impl Provider {
227    async fn fetch_inner(
228        client: Client,
229        request: Request,
230    ) -> Result<(String, Bytes), ProviderError> {
231        let response = Self::fetch_response_inner(client, request).await?;
232        Ok((response.url.to_string(), response.body))
233    }
234
235    #[allow(clippy::type_complexity)]
236    pub fn fetch_with_callback(
237        &self,
238        request: Request,
239        callback: Box<dyn FnOnce(Result<(String, Bytes), ProviderError>) + Send + Sync + 'static>,
240    ) {
241        #[cfg(feature = "tracing")]
242        let url = request.url.to_string();
243
244        let client = self.client.clone();
245        spawn(async move {
246            let result = Self::fetch_inner(client, request).await;
247
248            #[cfg(feature = "tracing")]
249            if let Err(e) = &result {
250                tracing::error!(url = url.as_str(), error = ?e, "Fetching");
251            } else {
252                tracing::info!(url = url.as_str(), "Success fetching");
253            }
254
255            callback(result);
256        });
257    }
258
259    pub async fn fetch_async(&self, request: Request) -> Result<(String, Bytes), ProviderError> {
260        #[cfg(feature = "tracing")]
261        let url = request.url.to_string();
262
263        let result = Self::fetch_inner(self.client.clone(), request).await;
264
265        #[cfg(feature = "tracing")]
266        if let Err(e) = &result {
267            tracing::error!(url = url.as_str(), error = ?e, "Fetching");
268        } else {
269            tracing::info!(url = url.as_str(), "Success fetching");
270        }
271
272        result
273    }
274
275    /// Fetch, keeping the response metadata that [`Provider::fetch_async`]
276    /// discards.
277    ///
278    /// HTTP error statuses remain errors on this document-loading API.
279    /// `FetchProvider` instead delivers every HTTP response, including errors.
280    /// A `data:` URL supplies its own Content-Type; a `file:` URL has none.
281    pub async fn fetch_response_async(
282        &self,
283        request: Request,
284    ) -> Result<FetchResponse, ProviderError> {
285        Self::fetch_response_inner(self.client.clone(), request).await
286    }
287
288    async fn fetch_response_inner(
289        client: Client,
290        request: Request,
291    ) -> Result<FetchResponse, ProviderError> {
292        let url = request.url.clone();
293        match url.scheme() {
294            "data" => {
295                // Scoped so the borrow of `url` ends before it is moved into
296                // the response.
297                let (body, headers) = {
298                    let data_url = DataUrl::process(url.as_str())?;
299                    let decoded = data_url.decode_to_vec()?;
300                    let mut headers = HeaderMap::new();
301                    if let Ok(value) = data_url.mime_type().to_string().parse() {
302                        headers.insert(CONTENT_TYPE, value);
303                    }
304                    (Bytes::from(decoded.0), headers)
305                };
306                Ok(FetchResponse::new(url, StatusCode::OK)
307                    .headers(headers)
308                    .body(body))
309            }
310            "file" => {
311                let file_content = std::fs::read(url.path())?;
312                Ok(FetchResponse::new(url, StatusCode::OK).body(Bytes::from(file_content)))
313            }
314            "http" | "https" => {
315                let mut wire_request = FetchRequest::get(request.url).method(request.method);
316                wire_request.headers = request.headers;
317                let content_type = request.content_type.or_else(|| {
318                    wire_request
319                        .headers
320                        .get(CONTENT_TYPE)
321                        .and_then(|value| value.to_str().ok())
322                        .map(str::to_owned)
323                });
324                if let Some(content_type) = &content_type {
325                    let value = HeaderValue::from_str(content_type)
326                        .map_err(|error| ProviderError::InvalidRequest(error.to_string()))?;
327                    wire_request.headers.insert(CONTENT_TYPE, value);
328                }
329                wire_request.body = encode_body(
330                    request.body,
331                    content_type.as_deref(),
332                    &mut wire_request.headers,
333                )?;
334                let response = client
335                    .fetch(wire_request)
336                    .await
337                    .map_err(|error| ProviderError::Network(error.to_string()))?;
338
339                if !response.status.is_success() {
340                    #[cfg(feature = "tracing")]
341                    tracing::warn!(
342                        url = response.url.as_str(),
343                        status = response.status.as_u16(),
344                        "HTTP error status"
345                    );
346                    return Err(ProviderError::HttpStatus {
347                        status: response.status,
348                        url: response.url.to_string(),
349                    });
350                }
351                Ok(response)
352            }
353            scheme => Err(ProviderError::UnsupportedScheme(scheme.to_owned())),
354        }
355    }
356}
357
358/// The `fetch()` path.
359///
360/// Separate from [`Provider::fetch_inner`] rather than layered on it, and the
361/// reason is the whole point of the trait: `fetch_inner` returns
362/// `(String, Bytes)` and turns any non-success status into a `ProviderError`
363/// that [`NetProvider::fetch`] then logs and drops. A caller loading an image
364/// wants exactly that. A `fetch()` caller wants the 404.
365impl Provider {
366    async fn platform_fetch_inner(
367        client: Client,
368        request: FetchRequest,
369    ) -> Result<FetchResponse, FetchError> {
370        match request.url.scheme() {
371            "data" => Self::platform_fetch_data(request),
372            "file" => Self::platform_fetch_file(request),
373            "http" | "https" => client
374                .fetch(request)
375                .await
376                .map_err(|error| FetchError::Network(error.to_string())),
377            scheme => Err(FetchError::UnsupportedScheme(scheme.to_owned())),
378        }
379    }
380
381    /// A `data:` URL, answered as a synthetic 200.
382    ///
383    /// The status and the `Content-Type` are invented, because a data URL has
384    /// no server to supply them, and inventing them is what the fetch
385    /// specification requires: a `data:` response is a 200 whose content type
386    /// is the one encoded in the URL.
387    fn platform_fetch_data(request: FetchRequest) -> Result<FetchResponse, FetchError> {
388        let data_url = DataUrl::process(request.url.as_str())
389            .map_err(|err| FetchError::InvalidRequest(format!("{err:?}")))?;
390        let mime = data_url.mime_type().to_string();
391        let (body, _) = data_url
392            .decode_to_vec()
393            .map_err(|err| FetchError::InvalidRequest(format!("{err:?}")))?;
394
395        let mut headers = HeaderMap::new();
396        if let Ok(value) = mime.parse() {
397            headers.insert(CONTENT_TYPE, value);
398        }
399
400        Ok(FetchResponse::new(request.url, StatusCode::OK)
401            .headers(headers)
402            .body(Bytes::from(body)))
403    }
404
405    /// A `file:` URL, answered as a synthetic 200.
406    ///
407    /// Goes through [`Url::to_file_path`] rather than `Url::path`, which is
408    /// what [`Provider::fetch_inner`] uses. `path` hands back the URL's
409    /// percent-encoded path component as a string, so a file whose name
410    /// contains a space or a `#` is looked up under the wrong name, and on
411    /// Windows the leading slash makes it wrong outright. `to_file_path`
412    /// decodes and refuses a URL that does not name a local path.
413    ///
414    /// **This is not access control, and it is not claiming to be.** Whether a
415    /// document may read local files at all is an origin question, and origins
416    /// are not visible here; see `blitz-platform-api`, which holds the origin
417    /// and is where such a policy belongs.
418    fn platform_fetch_file(request: FetchRequest) -> Result<FetchResponse, FetchError> {
419        let path = request.url.to_file_path().map_err(|()| {
420            FetchError::InvalidRequest(format!("not a local path: {}", request.url))
421        })?;
422
423        let body = std::fs::read(path).map_err(|err| FetchError::Network(err.to_string()))?;
424
425        Ok(FetchResponse::new(request.url, StatusCode::OK).body(Bytes::from(body)))
426    }
427}
428
429impl FetchProvider for Provider {
430    fn fetch(&self, request: FetchRequest, handler: Box<dyn FetchHandler>) {
431        let client = self.client.clone();
432
433        #[cfg(feature = "tracing")]
434        let url = request.url.to_string();
435
436        spawn(async move {
437            let result = Self::platform_fetch_inner(client, request).await;
438
439            #[cfg(feature = "tracing")]
440            match &result {
441                Ok(response) => tracing::info!(
442                    url = url.as_str(),
443                    status = response.status.as_u16(),
444                    "fetch complete"
445                ),
446                Err(error) => tracing::error!(url = url.as_str(), error = ?error, "fetch failed"),
447            }
448
449            handler.complete(result);
450        });
451    }
452}
453
454impl NetProvider for Provider {
455    fn fetch(&self, doc_id: usize, mut request: Request, handler: Box<dyn NetHandler>) {
456        let client = self.client.clone();
457
458        #[cfg(feature = "tracing")]
459        tracing::info!(url = request.url.as_str(), "Fetching");
460
461        let waker = self.waker.clone();
462        spawn(async move {
463            #[cfg(feature = "tracing")]
464            let url = request.url.to_string();
465
466            let signal = request.signal.take();
467            let result = if let Some(signal) = signal {
468                AbortFetch::new(
469                    signal,
470                    Box::pin(async move { Self::fetch_inner(client, request).await }),
471                )
472                .await
473            } else {
474                Self::fetch_inner(client, request).await
475            };
476
477            waker.wake(doc_id);
478
479            match result {
480                Ok((response_url, bytes)) => {
481                    handler.bytes(response_url, bytes);
482                    #[cfg(feature = "tracing")]
483                    tracing::info!(url = url.as_str(), "Success fetching");
484                }
485                Err(e) => {
486                    #[cfg(feature = "tracing")]
487                    tracing::error!(url = url.as_str(), error = ?e, "Error fetching");
488                    #[cfg(not(feature = "tracing"))]
489                    let _ = e;
490                }
491            };
492        });
493    }
494}
495
496struct AbortFetch<F, T> {
497    signal: AbortSignal,
498    future: F,
499    _rt: PhantomData<T>,
500}
501
502impl<F, T> AbortFetch<F, T> {
503    fn new(signal: AbortSignal, future: F) -> Self {
504        Self {
505            signal,
506            future,
507            _rt: PhantomData,
508        }
509    }
510}
511
512impl<F, T> Future for AbortFetch<F, T>
513where
514    F: Future + Unpin + 'static,
515    F::Output: Into<Result<T, ProviderError>> + 'static,
516    T: Unpin,
517{
518    type Output = Result<T, ProviderError>;
519
520    fn poll(
521        mut self: std::pin::Pin<&mut Self>,
522        cx: &mut std::task::Context<'_>,
523    ) -> std::task::Poll<Self::Output> {
524        if self.signal.aborted() {
525            return Poll::Ready(Err(ProviderError::Abort));
526        }
527
528        match Pin::new(&mut self.future).poll(cx) {
529            Poll::Ready(output) => Poll::Ready(output.into()),
530            Poll::Pending => Poll::Pending,
531        }
532    }
533}
534
535#[derive(Debug)]
536pub enum ProviderError {
537    Abort,
538    Io(std::io::Error),
539    DataUrl(data_url::DataUrlError),
540    DataUrlBase64(data_url::forgiving_base64::InvalidBase64),
541    Network(String),
542    InvalidRequest(String),
543    UnsupportedScheme(String),
544    HttpStatus { status: StatusCode, url: String },
545}
546
547impl std::fmt::Display for ProviderError {
548    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549        match self {
550            Self::Abort => write!(f, "request aborted"),
551            Self::Io(e) => write!(f, "io error: {e}"),
552            Self::DataUrl(e) => write!(f, "data url error: {e:?}"),
553            Self::DataUrlBase64(e) => write!(f, "data url base64 error: {e:?}"),
554            Self::Network(e) => write!(f, "network error: {e}"),
555            Self::InvalidRequest(e) => write!(f, "invalid request: {e}"),
556            Self::UnsupportedScheme(scheme) => write!(f, "unsupported URL scheme: {scheme}"),
557            Self::HttpStatus { status, url } => write!(f, "HTTP {status} for {url}"),
558        }
559    }
560}
561
562impl std::error::Error for ProviderError {}
563
564impl From<std::io::Error> for ProviderError {
565    fn from(value: std::io::Error) -> Self {
566        Self::Io(value)
567    }
568}
569
570impl From<data_url::DataUrlError> for ProviderError {
571    fn from(value: data_url::DataUrlError) -> Self {
572        Self::DataUrl(value)
573    }
574}
575
576impl From<data_url::forgiving_base64::InvalidBase64> for ProviderError {
577    fn from(value: data_url::forgiving_base64::InvalidBase64) -> Self {
578        Self::DataUrlBase64(value)
579    }
580}
581
582fn encode_body(
583    body: Body,
584    content_type: Option<&str>,
585    headers: &mut HeaderMap,
586) -> Result<Option<Bytes>, ProviderError> {
587    match body {
588        Body::Bytes(bytes) => Ok(Some(bytes)),
589        Body::Empty => Ok(None),
590        Body::Form(form) => {
591            let mime = content_type
592                .and_then(|value| value.split(';').next())
593                .map(str::trim)
594                .unwrap_or("");
595            if mime.eq_ignore_ascii_case("application/x-www-form-urlencoded") {
596                let mut encoded = url::form_urlencoded::Serializer::new(String::new());
597                for entry in &form.0 {
598                    encoded.append_pair(&entry.name, entry.value.as_ref());
599                }
600                return Ok(Some(Bytes::from(encoded.finish())));
601            }
602            if mime.eq_ignore_ascii_case("multipart/form-data") {
603                #[cfg(all(feature = "multipart", not(target_arch = "wasm32")))]
604                {
605                    return encode_multipart(form, headers).map(Some);
606                }
607                #[cfg(not(all(feature = "multipart", not(target_arch = "wasm32"))))]
608                {
609                    let _ = headers;
610                    return Err(ProviderError::InvalidRequest(
611                        "multipart requires the native multipart feature".to_owned(),
612                    ));
613                }
614            }
615            Err(ProviderError::InvalidRequest(
616                "unsupported form Content-Type".to_owned(),
617            ))
618        }
619    }
620}
621
622#[cfg(all(feature = "multipart", not(target_arch = "wasm32")))]
623fn encode_multipart(
624    form: blitz_traits::net::FormData,
625    headers: &mut HeaderMap,
626) -> Result<Bytes, ProviderError> {
627    use blitz_traits::net::{Entry, EntryValue};
628    use psc_nanoid::{Nanoid, alphabet::Base64UrlAlphabet};
629    use std::io::Read;
630
631    let boundary = Nanoid::<21, Base64UrlAlphabet>::new().to_string();
632    let mut encoded = Vec::new();
633    for Entry { name, value } in form.0 {
634        let mut head = format!(
635            "--{boundary}\r\nContent-Disposition: form-data; name=\"{}\"",
636            multipart_parameter(&name),
637        );
638        let body = match value {
639            EntryValue::String(value) => Bytes::from(value),
640            EntryValue::File(path) => {
641                let name = path.file_name().unwrap_or_default().to_string_lossy();
642                head.push_str(&format!(
643                    "; filename=\"{}\"\r\nContent-Type: application/octet-stream",
644                    multipart_parameter(&name),
645                ));
646                let file = std::fs::File::open(path)?;
647                let mut body = Vec::new();
648                file.take(h1::MAX_BODY_BYTES as u64 + 1)
649                    .read_to_end(&mut body)?;
650                if body.len() > h1::MAX_BODY_BYTES {
651                    return Err(ProviderError::InvalidRequest(
652                        "multipart file exceeds the body limit".to_owned(),
653                    ));
654                }
655                Bytes::from(body)
656            }
657            EntryValue::EmptyFile => {
658                head.push_str("; filename=\"\"\r\nContent-Type: application/octet-stream");
659                Bytes::new()
660            }
661        };
662        head.push_str("\r\n\r\n");
663        let required = head
664            .len()
665            .checked_add(body.len())
666            .and_then(|length| length.checked_add(2))
667            .and_then(|length| length.checked_add(encoded.len()))
668            .ok_or_else(|| {
669                ProviderError::InvalidRequest("multipart body is too large".to_owned())
670            })?;
671        if required > h1::MAX_BODY_BYTES {
672            return Err(ProviderError::InvalidRequest(
673                "multipart body exceeds the body limit".to_owned(),
674            ));
675        }
676        encoded.extend_from_slice(head.as_bytes());
677        encoded.extend_from_slice(&body);
678        encoded.extend_from_slice(b"\r\n");
679    }
680    let end = format!("--{boundary}--\r\n");
681    if end.len() > h1::MAX_BODY_BYTES.saturating_sub(encoded.len()) {
682        return Err(ProviderError::InvalidRequest(
683            "multipart body exceeds the body limit".to_owned(),
684        ));
685    }
686    encoded.extend_from_slice(end.as_bytes());
687    let content_type = HeaderValue::from_str(&format!("multipart/form-data; boundary={boundary}"))
688        .map_err(|error| ProviderError::InvalidRequest(error.to_string()))?;
689    headers.insert(CONTENT_TYPE, content_type);
690    Ok(Bytes::from(encoded))
691}
692
693#[cfg(all(feature = "multipart", not(target_arch = "wasm32")))]
694fn multipart_parameter(value: &str) -> String {
695    value
696        .replace('\r', "%0D")
697        .replace('\n', "%0A")
698        .replace('"', "%22")
699}
700
701struct DummyNetWaker;
702impl NetWaker for DummyNetWaker {
703    fn wake(&self, _client_id: usize) {}
704}