Skip to content

Commit e9eddf0

Browse files
authored
Turbopack: Make turbo-tasks-fetch a bit more OOP-like (vercel#81995)
This was a request from @lukesandberg to make this API a bit more object-oriented: vercel#81818 (comment) - `ReqwestClientConfig` is now just `FetchClient`. - `fetch` is a method on `FetchClient` instead of a function taking a config. - **Bonus change:** `lib.rs` is now a tiny stub that just re-exports things from other modules (`client.rs`, `error.rs`, `response.rs`).
1 parent 1964b17 commit e9eddf0

File tree

10 files changed

+376
-364
lines changed

10 files changed

+376
-364
lines changed

crates/next-core/src/next_config.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use turbo_tasks::{
99
trace::TraceRawVcs,
1010
};
1111
use turbo_tasks_env::{EnvMap, ProcessEnv};
12-
use turbo_tasks_fetch::ReqwestClientConfig;
12+
use turbo_tasks_fetch::FetchClient;
1313
use turbo_tasks_fs::FileSystemPath;
1414
use turbopack::module_options::{
1515
ConditionItem, ConditionPath, LoaderRuleItem, OptionWebpackRules,
@@ -1725,10 +1725,7 @@ impl NextConfig {
17251725
}
17261726

17271727
#[turbo_tasks::function]
1728-
pub async fn reqwest_client_config(
1729-
&self,
1730-
env: Vc<Box<dyn ProcessEnv>>,
1731-
) -> Result<Vc<ReqwestClientConfig>> {
1728+
pub async fn fetch_client(&self, env: Vc<Box<dyn ProcessEnv>>) -> Result<Vc<FetchClient>> {
17321729
// Support both an env var and the experimental flag to provide more flexibility to
17331730
// developers on locked down systems, depending on if they want to configure this on a
17341731
// per-system or per-project basis.
@@ -1742,7 +1739,7 @@ impl NextConfig {
17421739
})
17431740
.or(self.experimental.turbopack_use_system_tls_certs)
17441741
.unwrap_or(false);
1745-
Ok(ReqwestClientConfig {
1742+
Ok(FetchClient {
17461743
tls_built_in_webpki_certs: !use_system_tls_certs,
17471744
tls_built_in_native_certs: use_system_tls_certs,
17481745
}

crates/next-core/src/next_font/google/mod.rs

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use turbo_rcstr::{RcStr, rcstr};
99
use turbo_tasks::{Completion, FxIndexMap, ResolvedVc, Vc};
1010
use turbo_tasks_bytes::stream::SingleValue;
1111
use turbo_tasks_env::{CommandLineProcessEnv, ProcessEnv};
12-
use turbo_tasks_fetch::{HttpResponseBody, ReqwestClientConfig, fetch};
12+
use turbo_tasks_fetch::{FetchClient, HttpResponseBody};
1313
use turbo_tasks_fs::{
1414
DiskFileSystem, File, FileContent, FileSystem, FileSystemPath,
1515
json::parse_json_with_source_context,
@@ -182,7 +182,7 @@ pub struct NextFontGoogleCssModuleReplacer {
182182
project_path: FileSystemPath,
183183
execution_context: ResolvedVc<ExecutionContext>,
184184
next_mode: ResolvedVc<NextMode>,
185-
reqwest_client_config: ResolvedVc<ReqwestClientConfig>,
185+
fetch_client: ResolvedVc<FetchClient>,
186186
}
187187

188188
#[turbo_tasks::value_impl]
@@ -192,13 +192,13 @@ impl NextFontGoogleCssModuleReplacer {
192192
project_path: FileSystemPath,
193193
execution_context: ResolvedVc<ExecutionContext>,
194194
next_mode: ResolvedVc<NextMode>,
195-
reqwest_client_config: ResolvedVc<ReqwestClientConfig>,
195+
fetch_client: ResolvedVc<FetchClient>,
196196
) -> Vc<Self> {
197197
Self::cell(NextFontGoogleCssModuleReplacer {
198198
project_path,
199199
execution_context,
200200
next_mode,
201-
reqwest_client_config,
201+
fetch_client,
202202
})
203203
}
204204

@@ -233,9 +233,9 @@ impl NextFontGoogleCssModuleReplacer {
233233
.map_or_else(
234234
|| {
235235
fetch_real_stylesheet(
236+
*self.fetch_client,
236237
stylesheet_url.clone(),
237238
css_virtual_path.clone(),
238-
*self.reqwest_client_config,
239239
)
240240
.boxed()
241241
},
@@ -375,19 +375,16 @@ struct NextFontGoogleFontFileOptions {
375375
#[turbo_tasks::value(shared)]
376376
pub struct NextFontGoogleFontFileReplacer {
377377
project_path: FileSystemPath,
378-
reqwest_client_config: ResolvedVc<ReqwestClientConfig>,
378+
fetch_client: ResolvedVc<FetchClient>,
379379
}
380380

381381
#[turbo_tasks::value_impl]
382382
impl NextFontGoogleFontFileReplacer {
383383
#[turbo_tasks::function]
384-
pub fn new(
385-
project_path: FileSystemPath,
386-
reqwest_client_config: ResolvedVc<ReqwestClientConfig>,
387-
) -> Vc<Self> {
384+
pub fn new(project_path: FileSystemPath, fetch_client: ResolvedVc<FetchClient>) -> Vc<Self> {
388385
Self::cell(NextFontGoogleFontFileReplacer {
389386
project_path,
390-
reqwest_client_config,
387+
fetch_client,
391388
})
392389
}
393390
}
@@ -443,12 +440,9 @@ impl ImportMappingReplacement for NextFontGoogleFontFileReplacer {
443440

444441
// doesn't seem ideal to download the font into a string, but probably doesn't
445442
// really matter either.
446-
let Some(font) = fetch_from_google_fonts(
447-
url.into(),
448-
font_virtual_path.clone(),
449-
*self.reqwest_client_config,
450-
)
451-
.await?
443+
let Some(font) =
444+
fetch_from_google_fonts(*self.fetch_client, url.into(), font_virtual_path.clone())
445+
.await?
452446
else {
453447
return Ok(ImportMapResult::Result(ResolveResult::unresolvable()).cell());
454448
};
@@ -670,27 +664,23 @@ fn font_file_options_from_query_map(query: &RcStr) -> Result<NextFontGoogleFontF
670664
}
671665

672666
async fn fetch_real_stylesheet(
667+
fetch_client: Vc<FetchClient>,
673668
stylesheet_url: RcStr,
674669
css_virtual_path: FileSystemPath,
675-
reqwest_client_config: Vc<ReqwestClientConfig>,
676670
) -> Result<Option<Vc<RcStr>>> {
677-
let body =
678-
fetch_from_google_fonts(stylesheet_url, css_virtual_path, reqwest_client_config).await?;
671+
let body = fetch_from_google_fonts(fetch_client, stylesheet_url, css_virtual_path).await?;
679672

680673
Ok(body.map(|body| body.to_string()))
681674
}
682675

683676
async fn fetch_from_google_fonts(
677+
fetch_client: Vc<FetchClient>,
684678
url: RcStr,
685679
virtual_path: FileSystemPath,
686-
reqwest_client_config: Vc<ReqwestClientConfig>,
687680
) -> Result<Option<Vc<HttpResponseBody>>> {
688-
let result = fetch(
689-
url,
690-
Some(rcstr!(USER_AGENT_FOR_GOOGLE_FONTS)),
691-
reqwest_client_config,
692-
)
693-
.await?;
681+
let result = fetch_client
682+
.fetch(url, Some(rcstr!(USER_AGENT_FOR_GOOGLE_FONTS)))
683+
.await?;
694684

695685
Ok(match *result {
696686
Ok(r) => Some(*r.await?.body),

crates/next-core/src/next_import_map.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,15 +1022,15 @@ async fn insert_next_shared_aliases(
10221022
next_font_google_replacer_mapping,
10231023
);
10241024

1025-
let reqwest_client_config = next_config.reqwest_client_config(execution_context.env());
1025+
let fetch_client = next_config.fetch_client(execution_context.env());
10261026
import_map.insert_alias(
10271027
AliasPattern::exact("@vercel/turbopack-next/internal/font/google/cssmodule.module.css"),
10281028
ImportMapping::Dynamic(ResolvedVc::upcast(
10291029
NextFontGoogleCssModuleReplacer::new(
10301030
project_path.clone(),
10311031
execution_context,
10321032
next_mode,
1033-
reqwest_client_config,
1033+
fetch_client,
10341034
)
10351035
.to_resolved()
10361036
.await?,
@@ -1041,7 +1041,7 @@ async fn insert_next_shared_aliases(
10411041
import_map.insert_alias(
10421042
AliasPattern::exact(GOOGLE_FONTS_INTERNAL_PREFIX),
10431043
ImportMapping::Dynamic(ResolvedVc::upcast(
1044-
NextFontGoogleFontFileReplacer::new(project_path.clone(), reqwest_client_config)
1044+
NextFontGoogleFontFileReplacer::new(project_path.clone(), fetch_client)
10451045
.to_resolved()
10461046
.await?,
10471047
))

turbopack/crates/turbo-tasks-fetch/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ workspace = true
2020
[target.'cfg(all(target_os = "windows", target_arch = "aarch64"))'.dependencies]
2121
reqwest = { workspace = true, features = ["native-tls"] }
2222

23-
# If you modify this cfg, make sure you update `ReqwestClientConfig::try_build`.
23+
# If you modify this cfg, make sure you update `FetchClient::try_build_uncached_reqwest_client`.
2424
[target.'cfg(not(any(all(target_os = "windows", target_arch = "aarch64"), target_arch="wasm32")))'.dependencies]
2525
reqwest = { workspace = true, features = ["rustls-tls-webpki-roots", "rustls-tls-native-roots"] }
2626

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
use std::{hash::Hash, sync::LazyLock};
2+
3+
use anyhow::Result;
4+
use quick_cache::sync::Cache;
5+
use serde::{Deserialize, Serialize};
6+
use turbo_rcstr::RcStr;
7+
use turbo_tasks::{
8+
NonLocalValue, ReadRef, Vc, duration_span, mark_session_dependent, trace::TraceRawVcs,
9+
};
10+
11+
use crate::{FetchError, FetchResult, HttpResponse, HttpResponseBody};
12+
13+
const MAX_CLIENTS: usize = 16;
14+
static CLIENT_CACHE: LazyLock<Cache<ReadRef<FetchClient>, reqwest::Client>> =
15+
LazyLock::new(|| Cache::new(MAX_CLIENTS));
16+
17+
#[derive(Hash, PartialEq, Eq, Serialize, Deserialize, NonLocalValue, Debug, TraceRawVcs)]
18+
pub enum ProxyConfig {
19+
Http(RcStr),
20+
Https(RcStr),
21+
}
22+
23+
/// Represents the configuration needed to construct a [`reqwest::Client`].
24+
///
25+
/// This is used to cache clients keyed by their configuration, so the configuration should contain
26+
/// as few fields as possible and change infrequently.
27+
///
28+
/// This is needed because [`reqwest::ClientBuilder`] does not implement the required traits. This
29+
/// factory cannot be a closure because closures do not implement `Eq` or `Hash`.
30+
#[turbo_tasks::value(shared)]
31+
#[derive(Hash)]
32+
pub struct FetchClient {
33+
/// Whether to load embedded webpki root certs with rustls. Default is true.
34+
///
35+
/// Ignored for:
36+
/// - Windows on ARM, which uses `native-tls` instead of `rustls-tls`.
37+
/// - Ignored for WASM targets, which use the runtime's TLS implementation.
38+
pub tls_built_in_webpki_certs: bool,
39+
/// Whether to load native root certs using the `rustls-native-certs` crate. This may make
40+
/// reqwest client initialization slower, so it's not used by default.
41+
///
42+
/// Ignored for:
43+
/// - Windows on ARM, which uses `native-tls` instead of `rustls-tls`.
44+
/// - Ignored for WASM targets, which use the runtime's TLS implementation.
45+
pub tls_built_in_native_certs: bool,
46+
}
47+
48+
impl Default for FetchClient {
49+
fn default() -> Self {
50+
Self {
51+
tls_built_in_webpki_certs: true,
52+
tls_built_in_native_certs: false,
53+
}
54+
}
55+
}
56+
57+
impl FetchClient {
58+
/// Returns a cached instance of `reqwest::Client` it exists, otherwise constructs a new one.
59+
///
60+
/// The cache is bound in size to prevent accidental blowups or leaks. However, in practice,
61+
/// very few clients should be created, likely only when the bundler configuration changes.
62+
///
63+
/// Client construction is largely deterministic, aside from changes to system TLS
64+
/// configuration.
65+
///
66+
/// The reqwest client fails to construct if the TLS backend cannot be initialized, or the
67+
/// resolver cannot load the system configuration. These failures should be treated as
68+
/// cached for some amount of time, but ultimately transient (e.g. using
69+
/// [`turbo_tasks::mark_session_dependent`]).
70+
pub fn try_get_cached_reqwest_client(
71+
self: ReadRef<FetchClient>,
72+
) -> reqwest::Result<reqwest::Client> {
73+
CLIENT_CACHE.get_or_insert_with(&self, {
74+
let this = ReadRef::clone(&self);
75+
move || this.try_build_uncached_reqwest_client()
76+
})
77+
}
78+
79+
fn try_build_uncached_reqwest_client(&self) -> reqwest::Result<reqwest::Client> {
80+
let client_builder = reqwest::Client::builder();
81+
82+
// make sure this cfg matches the one in `Cargo.toml`!
83+
#[cfg(not(any(
84+
all(target_os = "windows", target_arch = "aarch64"),
85+
target_arch = "wasm32"
86+
)))]
87+
let client_builder = client_builder
88+
.tls_built_in_root_certs(false)
89+
.tls_built_in_webpki_certs(self.tls_built_in_webpki_certs)
90+
.tls_built_in_native_certs(self.tls_built_in_native_certs);
91+
92+
client_builder.build()
93+
}
94+
}
95+
96+
#[turbo_tasks::value_impl]
97+
impl FetchClient {
98+
#[turbo_tasks::function(network)]
99+
pub async fn fetch(
100+
self: Vc<FetchClient>,
101+
url: RcStr,
102+
user_agent: Option<RcStr>,
103+
) -> Result<Vc<FetchResult>> {
104+
let url_ref = &*url;
105+
let this = self.await?;
106+
let response_result: reqwest::Result<HttpResponse> = async move {
107+
let reqwest_client = this.try_get_cached_reqwest_client()?;
108+
109+
let mut builder = reqwest_client.get(url_ref);
110+
if let Some(user_agent) = user_agent {
111+
builder = builder.header("User-Agent", user_agent.as_str());
112+
}
113+
114+
let response = {
115+
let _span = duration_span!("fetch request", url = url_ref);
116+
builder.send().await
117+
}
118+
.and_then(|r| r.error_for_status())?;
119+
120+
let status = response.status().as_u16();
121+
122+
let body = {
123+
let _span = duration_span!("fetch response", url = url_ref);
124+
response.bytes().await?
125+
}
126+
.to_vec();
127+
128+
Ok(HttpResponse {
129+
status,
130+
body: HttpResponseBody(body).resolved_cell(),
131+
})
132+
}
133+
.await;
134+
135+
match response_result {
136+
Ok(resp) => Ok(Vc::cell(Ok(resp.resolved_cell()))),
137+
Err(err) => {
138+
// the client failed to construct or the HTTP request failed
139+
mark_session_dependent();
140+
Ok(Vc::cell(Err(
141+
FetchError::from_reqwest_error(&err, &url).resolved_cell()
142+
)))
143+
}
144+
}
145+
}
146+
}
147+
148+
#[doc(hidden)]
149+
pub fn __test_only_reqwest_client_cache_clear() {
150+
CLIENT_CACHE.clear()
151+
}
152+
153+
#[doc(hidden)]
154+
pub fn __test_only_reqwest_client_cache_len() -> usize {
155+
CLIENT_CACHE.len()
156+
}

0 commit comments

Comments
 (0)