Skip to main content

turbo_tasks_fetch/
client.rs

1use std::{hash::Hash, sync::LazyLock};
2
3use anyhow::Result;
4use quick_cache::sync::Cache;
5use turbo_rcstr::RcStr;
6use turbo_tasks::{ReadRef, Vc, duration_span, mark_session_dependent};
7
8use crate::{FetchError, FetchResult, HttpResponse, HttpResponseBody};
9
10const MAX_CLIENTS: usize = 16;
11static CLIENT_CACHE: LazyLock<Cache<ReadRef<FetchClientConfig>, reqwest::Client>> =
12    LazyLock::new(|| Cache::new(MAX_CLIENTS));
13
14/// Represents the configuration needed to construct a [`reqwest::Client`].
15///
16/// This is used to cache clients keyed by their configuration, so the configuration should contain
17/// as few fields as possible and change infrequently.
18///
19/// This is needed because [`reqwest::ClientBuilder`] does not implement the required traits. This
20/// factory cannot be a closure because closures do not implement `Eq` or `Hash`.
21#[turbo_tasks::value(shared)]
22#[derive(Hash, Default)]
23pub struct FetchClientConfig {}
24
25impl FetchClientConfig {
26    /// Returns a cached instance of `reqwest::Client` it exists, otherwise constructs a new one.
27    ///
28    /// The cache is bound in size to prevent accidental blowups or leaks. However, in practice,
29    /// very few clients should be created, likely only when the bundler configuration changes.
30    ///
31    /// Client construction is largely deterministic, aside from changes to system TLS
32    /// configuration.
33    ///
34    /// The reqwest client fails to construct if the TLS backend cannot be initialized, or the
35    /// resolver cannot load the system configuration. These failures should be treated as
36    /// cached for some amount of time, but ultimately transient (e.g. using
37    /// [`turbo_tasks::mark_session_dependent`]).
38    pub fn try_get_cached_reqwest_client(
39        self: ReadRef<FetchClientConfig>,
40    ) -> reqwest::Result<reqwest::Client> {
41        CLIENT_CACHE.get_or_insert_with(&self, {
42            let this = ReadRef::clone(&self);
43            move || this.try_build_uncached_reqwest_client()
44        })
45    }
46
47    fn try_build_uncached_reqwest_client(&self) -> reqwest::Result<reqwest::Client> {
48        #[allow(unused_mut)]
49        let mut builder = reqwest::Client::builder();
50        #[cfg(any(target_os = "linux", all(windows, not(target_arch = "aarch64"))))]
51        {
52            use std::sync::Once;
53            static ONCE: Once = Once::new();
54            ONCE.call_once(|| {
55                rustls::crypto::ring::default_provider()
56                    .install_default()
57                    .unwrap()
58            });
59            builder = builder.tls_backend_rustls();
60        }
61        #[cfg(all(windows, target_arch = "aarch64"))]
62        {
63            builder = builder.tls_backend_native();
64        }
65        #[cfg(target_os = "linux")]
66        {
67            // Add webpki_root_certs on Linux (in addition to reqwest's default
68            // `rustls-platform-verifier`), in case the user is building in a bare-bones docker
69            // image that does not contain any root certs (e.g. `oven/bun:slim`).
70            builder = builder.tls_certs_merge(webpki_root_certs::TLS_SERVER_ROOT_CERTS.iter().map(
71                |der| {
72                    reqwest::Certificate::from_der(der)
73                        .expect("webpki_root_certs should parse correctly")
74                },
75            ))
76        }
77        builder.build()
78    }
79}
80
81#[turbo_tasks::value_impl]
82impl FetchClientConfig {
83    #[turbo_tasks::function(network)]
84    pub async fn fetch(
85        self: Vc<FetchClientConfig>,
86        url: RcStr,
87        user_agent: Option<RcStr>,
88    ) -> Result<Vc<FetchResult>> {
89        let url_ref = &*url;
90        let this = self.await?;
91        let response_result: reqwest::Result<HttpResponse> = async move {
92            let reqwest_client = this.try_get_cached_reqwest_client()?;
93
94            let mut builder = reqwest_client.get(url_ref);
95            if let Some(user_agent) = user_agent {
96                builder = builder.header("User-Agent", user_agent.as_str());
97            }
98
99            let response = {
100                let _span = duration_span!("fetch request", url = url_ref);
101                builder.send().await
102            }
103            .and_then(|r| r.error_for_status())?;
104
105            let status = response.status().as_u16();
106
107            let body = {
108                let _span = duration_span!("fetch response", url = url_ref);
109                response.bytes().await?
110            }
111            .to_vec();
112
113            Ok(HttpResponse {
114                status,
115                body: HttpResponseBody(body).resolved_cell(),
116            })
117        }
118        .await;
119
120        match response_result {
121            Ok(resp) => Ok(Vc::cell(Ok(resp.resolved_cell()))),
122            Err(err) => {
123                // the client failed to construct or the HTTP request failed
124                mark_session_dependent();
125                Ok(Vc::cell(Err(
126                    FetchError::from_reqwest_error(&err, &url).resolved_cell()
127                )))
128            }
129        }
130    }
131}
132
133#[doc(hidden)]
134pub fn __test_only_reqwest_client_cache_clear() {
135    CLIENT_CACHE.clear()
136}
137
138#[doc(hidden)]
139pub fn __test_only_reqwest_client_cache_len() -> usize {
140    CLIENT_CACHE.len()
141}