1use blitz_traits::net::{AbortSignal, Body, Bytes, NetHandler, NetProvider, NetWaker, Request};
6use data_url::DataUrl;
7use std::{
8 collections::HashMap,
9 marker::PhantomData,
10 pin::Pin,
11 sync::{Arc, Mutex},
12 task::Poll,
13};
14use tokio::sync::Semaphore;
15
16#[cfg(feature = "cache")]
17use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions};
18
19const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0";
20
21const PER_HOST_MAX_CONCURRENT: usize = 6;
23
24type HostLimits = Arc<Mutex<HashMap<String, Arc<Semaphore>>>>;
25
26#[cfg(feature = "cache")]
27type Client = reqwest_middleware::ClientWithMiddleware;
28#[cfg(not(feature = "cache"))]
29type Client = reqwest::Client;
30
31#[cfg(feature = "cache")]
32type RequestBuilder = reqwest_middleware::RequestBuilder;
33#[cfg(not(feature = "cache"))]
34type RequestBuilder = reqwest::RequestBuilder;
35
36#[cfg(feature = "cache")]
37fn get_cache_path() -> std::path::PathBuf {
38 use directories::ProjectDirs;
39 let path = ProjectDirs::from("com", "DioxusLabs", "Blitz")
40 .expect("Failed to find cache directory")
41 .cache_dir()
42 .to_owned();
43 #[cfg(feature = "tracing")]
44 tracing::info!(path = ?path.display(), "Using cache dir");
45 path
46}
47
48#[cfg(target_arch = "wasm32")]
49fn spawn(fut: impl Future + 'static) {
50 wasm_bindgen_futures::spawn_local(async move {
51 fut.await;
52 });
53}
54
55#[cfg(not(target_arch = "wasm32"))]
56fn spawn<F>(fut: F)
57where
58 F: Future + Send + 'static,
59 F::Output: Send + 'static,
60{
61 tokio::spawn(fut);
62}
63
64pub struct Provider {
65 client: Client,
66 waker: Arc<dyn NetWaker>,
67 per_host_limits: HostLimits,
68 #[cfg(feature = "cache")]
69 cache_manager: CACacheManager,
70}
71impl Provider {
72 pub fn new(waker: Option<Arc<dyn NetWaker>>) -> Self {
73 let builder = reqwest::Client::builder();
74 #[cfg(feature = "cookies")]
75 let builder = builder.cookie_store(true);
76 let client = builder.build().unwrap();
77
78 #[cfg(feature = "cache")]
79 let cache_manager = CACacheManager::new(get_cache_path(), true);
80
81 #[cfg(feature = "cache")]
82 let client = reqwest_middleware::ClientBuilder::new(client)
83 .with(Cache(HttpCache {
84 mode: CacheMode::Default,
85 manager: cache_manager.clone(),
86 options: HttpCacheOptions::default(),
87 }))
88 .build();
89
90 let waker = waker.unwrap_or(Arc::new(DummyNetWaker));
91 Self {
92 client,
93 waker,
94 per_host_limits: Arc::new(Mutex::new(HashMap::new())),
95 #[cfg(feature = "cache")]
96 cache_manager,
97 }
98 }
99 pub fn shared(waker: Option<Arc<dyn NetWaker>>) -> Arc<dyn NetProvider> {
100 Arc::new(Self::new(waker))
101 }
102 pub fn is_empty(&self) -> bool {
103 Arc::strong_count(&self.waker) == 1
104 }
105 pub fn count(&self) -> usize {
106 Arc::strong_count(&self.waker) - 1
107 }
108
109 #[cfg(feature = "cache")]
110 pub async fn clear_cache(&self) {
111 if let Err(e) = self.cache_manager.clear().await {
112 #[cfg(feature = "tracing")]
113 tracing::error!("Failed to clear HTTP cache: {:?}", e);
114 #[cfg(not(feature = "tracing"))]
115 let _ = e;
116 }
117 }
118}
119impl Provider {
120 async fn fetch_inner(
121 client: Client,
122 request: Request,
123 per_host_limits: HostLimits,
124 ) -> Result<(String, Bytes), ProviderError> {
125 match request.url.scheme() {
126 "data" => {
127 let data_url = DataUrl::process(request.url.as_str())?;
128 let decoded = data_url.decode_to_vec()?;
129 Ok((request.url.to_string(), Bytes::from(decoded.0)))
130 }
131 "file" => {
132 let file_content = std::fs::read(request.url.path())?;
133 Ok((request.url.to_string(), Bytes::from(file_content)))
134 }
135 _ => Self::fetch_http(client, request, per_host_limits).await,
136 }
137 }
138
139 async fn fetch_http(
140 client: Client,
141 request: Request,
142 per_host_limits: HostLimits,
143 ) -> Result<(String, Bytes), ProviderError> {
144 let host_key = request
147 .url
148 .host_str()
149 .map(str::to_owned)
150 .unwrap_or_default();
151 let semaphore = {
152 let mut map = per_host_limits.lock().unwrap();
153 map.entry(host_key)
154 .or_insert_with(|| Arc::new(Semaphore::new(PER_HOST_MAX_CONCURRENT)))
155 .clone()
156 };
157 let _permit = semaphore
158 .acquire()
159 .await
160 .expect("per-host semaphore was closed");
161
162 let mut req = client
163 .request(request.method, request.url)
164 .headers(request.headers)
165 .header("User-Agent", USER_AGENT);
166
167 if let Some(content_type) = request.content_type.as_ref() {
168 req = req.header("Content-Type", content_type);
169 }
170
171 let req = req
172 .apply_body(request.body, request.content_type.as_deref())
173 .await;
174 let response = req.send().await?;
175 let status = response.status();
176 let final_url = response.url().to_string();
177
178 if status.is_success() {
179 return Ok((final_url, response.bytes().await?));
180 }
181
182 #[cfg(feature = "tracing")]
183 tracing::warn!(
184 url = final_url.as_str(),
185 status = status.as_u16(),
186 "HTTP error status"
187 );
188 Err(ProviderError::HttpStatus {
189 status,
190 url: final_url,
191 })
192 }
193
194 #[allow(clippy::type_complexity)]
195 pub fn fetch_with_callback(
196 &self,
197 request: Request,
198 callback: Box<dyn FnOnce(Result<(String, Bytes), ProviderError>) + Send + Sync + 'static>,
199 ) {
200 #[cfg(feature = "tracing")]
201 let url = request.url.to_string();
202
203 let client = self.client.clone();
204 let per_host_limits = self.per_host_limits.clone();
205 spawn(async move {
206 let result = Self::fetch_inner(client, request, per_host_limits).await;
207
208 #[cfg(feature = "tracing")]
209 if let Err(e) = &result {
210 #[cfg(feature = "tracing")]
211 tracing::error!(url = url.as_str(), error = ?e, "Fetching");
212 } else {
213 #[cfg(feature = "tracing")]
214 tracing::info!(url = url.as_str(), "Success fetching");
215 }
216
217 callback(result);
218 });
219 }
220
221 pub async fn fetch_async(&self, request: Request) -> Result<(String, Bytes), ProviderError> {
222 #[cfg(feature = "tracing")]
223 let url = request.url.to_string();
224
225 let client = self.client.clone();
226 let per_host_limits = self.per_host_limits.clone();
227 let result = Self::fetch_inner(client, request, per_host_limits).await;
228
229 #[cfg(feature = "tracing")]
230 if let Err(e) = &result {
231 #[cfg(feature = "tracing")]
232 tracing::error!(url = url.as_str(), error = ?e, "Fetching");
233 } else {
234 #[cfg(feature = "tracing")]
235 tracing::info!(url = url.as_str(), "Success fetching");
236 }
237
238 result
239 }
240}
241
242impl NetProvider for Provider {
243 fn fetch(&self, doc_id: usize, mut request: Request, handler: Box<dyn NetHandler>) {
244 let client = self.client.clone();
245 let per_host_limits = self.per_host_limits.clone();
246
247 #[cfg(feature = "tracing")]
248 tracing::info!(url = request.url.as_str(), "Fetching");
249
250 let waker = self.waker.clone();
251 spawn(async move {
252 #[cfg(feature = "tracing")]
253 let url = request.url.to_string();
254
255 let signal = request.signal.take();
256 let result = if let Some(signal) = signal {
257 AbortFetch::new(
258 signal,
259 Box::pin(
260 async move { Self::fetch_inner(client, request, per_host_limits).await },
261 ),
262 )
263 .await
264 } else {
265 Self::fetch_inner(client, request, per_host_limits).await
266 };
267
268 waker.wake(doc_id);
269
270 match result {
271 Ok((response_url, bytes)) => {
272 handler.bytes(response_url, bytes);
273 #[cfg(feature = "tracing")]
274 tracing::info!(url = url.as_str(), "Success fetching");
275 }
276 Err(e) => {
277 #[cfg(feature = "tracing")]
278 tracing::error!(url = url.as_str(), error = ?e, "Error fetching");
279 #[cfg(not(feature = "tracing"))]
280 let _ = e;
281 }
282 };
283 });
284 }
285}
286
287struct AbortFetch<F, T> {
288 signal: AbortSignal,
289 future: F,
290 _rt: PhantomData<T>,
291}
292
293impl<F, T> AbortFetch<F, T> {
294 fn new(signal: AbortSignal, future: F) -> Self {
295 Self {
296 signal,
297 future,
298 _rt: PhantomData,
299 }
300 }
301}
302
303impl<F, T> Future for AbortFetch<F, T>
304where
305 F: Future + Unpin + 'static,
306 F::Output: Into<Result<T, ProviderError>> + 'static,
307 T: Unpin,
308{
309 type Output = Result<T, ProviderError>;
310
311 fn poll(
312 mut self: std::pin::Pin<&mut Self>,
313 cx: &mut std::task::Context<'_>,
314 ) -> std::task::Poll<Self::Output> {
315 if self.signal.aborted() {
316 return Poll::Ready(Err(ProviderError::Abort));
317 }
318
319 match Pin::new(&mut self.future).poll(cx) {
320 Poll::Ready(output) => Poll::Ready(output.into()),
321 Poll::Pending => Poll::Pending,
322 }
323 }
324}
325
326#[derive(Debug)]
327pub enum ProviderError {
328 Abort,
329 Io(std::io::Error),
330 DataUrl(data_url::DataUrlError),
331 DataUrlBase64(data_url::forgiving_base64::InvalidBase64),
332 ReqwestError(reqwest::Error),
333 #[cfg(feature = "cache")]
334 ReqwestMiddlewareError(reqwest_middleware::Error),
335 HttpStatus {
336 status: reqwest::StatusCode,
337 url: String,
338 },
339}
340
341impl std::fmt::Display for ProviderError {
342 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343 match self {
344 Self::Abort => write!(f, "request aborted"),
345 Self::Io(e) => write!(f, "io error: {e}"),
346 Self::DataUrl(e) => write!(f, "data url error: {e:?}"),
347 Self::DataUrlBase64(e) => write!(f, "data url base64 error: {e:?}"),
348 Self::ReqwestError(e) => write!(f, "reqwest error: {e}"),
349 #[cfg(feature = "cache")]
350 Self::ReqwestMiddlewareError(e) => write!(f, "reqwest middleware error: {e}"),
351 Self::HttpStatus { status, url } => write!(f, "HTTP {status} for {url}"),
352 }
353 }
354}
355
356impl From<std::io::Error> for ProviderError {
357 fn from(value: std::io::Error) -> Self {
358 Self::Io(value)
359 }
360}
361
362impl From<data_url::DataUrlError> for ProviderError {
363 fn from(value: data_url::DataUrlError) -> Self {
364 Self::DataUrl(value)
365 }
366}
367
368impl From<data_url::forgiving_base64::InvalidBase64> for ProviderError {
369 fn from(value: data_url::forgiving_base64::InvalidBase64) -> Self {
370 Self::DataUrlBase64(value)
371 }
372}
373
374impl From<reqwest::Error> for ProviderError {
375 fn from(value: reqwest::Error) -> Self {
376 Self::ReqwestError(value)
377 }
378}
379
380#[cfg(feature = "cache")]
381impl From<reqwest_middleware::Error> for ProviderError {
382 fn from(value: reqwest_middleware::Error) -> Self {
383 Self::ReqwestMiddlewareError(value)
384 }
385}
386
387trait ReqwestExt {
388 async fn apply_body(self, body: Body, content_type: Option<&str>) -> Self;
389}
390impl ReqwestExt for RequestBuilder {
391 async fn apply_body(self, body: Body, content_type: Option<&str>) -> Self {
392 match body {
393 Body::Bytes(bytes) => self.body(bytes),
394 Body::Form(form_data) => match content_type {
395 Some("application/x-www-form-urlencoded") => self.form(&form_data),
396 #[cfg(feature = "multipart")]
397 Some("multipart/form-data") => {
398 use blitz_traits::net::Entry;
399 use blitz_traits::net::EntryValue;
400 let mut form_data = form_data;
401 let mut form = reqwest::multipart::Form::new();
402 for Entry { name, value } in form_data.0.drain(..) {
403 form = match value {
404 EntryValue::String(value) => form.text(name, value),
405 EntryValue::File(path_buf) => form
406 .file(name, path_buf)
407 .await
408 .expect("Couldn't read form file from disk"),
409 EntryValue::EmptyFile => form.part(
410 name,
411 reqwest::multipart::Part::bytes(&[])
412 .mime_str("application/octet-stream")
413 .unwrap(),
414 ),
415 };
416 }
417 self.multipart(form)
418 }
419 _ => self,
420 },
421 Body::Empty => self,
422 }
423 }
424}
425
426struct DummyNetWaker;
427impl NetWaker for DummyNetWaker {
428 fn wake(&self, _client_id: usize) {}
429}