Skip to main content

boltz_http_client/
http_client.rs

1mod async_body;
2#[cfg(not(target_family = "wasm"))]
3pub mod github;
4#[cfg(not(target_family = "wasm"))]
5pub mod github_download;
6
7pub use anyhow::{Result, anyhow};
8pub use async_body::{AsyncBody, Inner, Json};
9use derive_more::Deref;
10use http::HeaderValue;
11pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
12
13use futures::future::BoxFuture;
14use parking_lot::Mutex;
15use std::sync::Arc;
16#[cfg(feature = "test-support")]
17use std::{any::type_name, fmt};
18pub use url::{Host, Url};
19
20#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
21pub enum RedirectPolicy {
22    #[default]
23    NoFollow,
24    FollowLimit(u32),
25    FollowAll,
26}
27pub struct FollowRedirects(pub bool);
28
29pub trait HttpRequestExt {
30    /// Conditionally modify self with the given closure.
31    fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
32    where
33        Self: Sized,
34    {
35        if condition { then(self) } else { self }
36    }
37
38    /// Conditionally unwrap and modify self with the given closure, if the given option is Some.
39    fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
40    where
41        Self: Sized,
42    {
43        match option {
44            Some(value) => then(self, value),
45            None => self,
46        }
47    }
48
49    /// Whether or not to follow redirects
50    fn follow_redirects(self, follow: RedirectPolicy) -> Self;
51}
52
53impl HttpRequestExt for http::request::Builder {
54    fn follow_redirects(self, follow: RedirectPolicy) -> Self {
55        self.extension(follow)
56    }
57}
58
59pub trait HttpClient: 'static + Send + Sync {
60    fn user_agent(&self) -> Option<&HeaderValue>;
61
62    fn proxy(&self) -> Option<&Url>;
63
64    fn send(
65        &self,
66        req: http::Request<AsyncBody>,
67    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
68
69    fn get(
70        &self,
71        uri: &str,
72        body: AsyncBody,
73        follow_redirects: bool,
74    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
75        let request = Builder::new()
76            .uri(uri)
77            .follow_redirects(if follow_redirects {
78                RedirectPolicy::FollowAll
79            } else {
80                RedirectPolicy::NoFollow
81            })
82            .body(body);
83
84        match request {
85            Ok(request) => self.send(request),
86            Err(e) => Box::pin(async move { Err(e.into()) }),
87        }
88    }
89
90    fn post_json(
91        &self,
92        uri: &str,
93        body: AsyncBody,
94    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
95        let request = Builder::new()
96            .uri(uri)
97            .method(Method::POST)
98            .header("Content-Type", "application/json")
99            .body(body);
100
101        match request {
102            Ok(request) => self.send(request),
103            Err(e) => Box::pin(async move { Err(e.into()) }),
104        }
105    }
106
107    #[cfg(feature = "test-support")]
108    fn as_fake(&self) -> &FakeHttpClient {
109        panic!("called as_fake on {}", type_name::<Self>())
110    }
111}
112
113/// An [`HttpClient`] that may have a proxy.
114#[derive(Deref)]
115pub struct HttpClientWithProxy {
116    #[deref]
117    client: Arc<dyn HttpClient>,
118    proxy: Option<Url>,
119}
120
121impl HttpClientWithProxy {
122    /// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
123    pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
124        let proxy_url = proxy_url
125            .and_then(|proxy| proxy.parse().ok())
126            .or_else(read_proxy_from_env);
127
128        Self::new_url(client, proxy_url)
129    }
130    pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
131        Self {
132            client,
133            proxy: proxy_url,
134        }
135    }
136}
137
138impl HttpClient for HttpClientWithProxy {
139    fn send(
140        &self,
141        req: Request<AsyncBody>,
142    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
143        self.client.send(req)
144    }
145
146    fn user_agent(&self) -> Option<&HeaderValue> {
147        self.client.user_agent()
148    }
149
150    fn proxy(&self) -> Option<&Url> {
151        self.proxy.as_ref()
152    }
153
154    #[cfg(feature = "test-support")]
155    fn as_fake(&self) -> &FakeHttpClient {
156        self.client.as_fake()
157    }
158}
159
160/// An [`HttpClient`] that has a base URL.
161#[derive(Deref)]
162pub struct HttpClientWithUrl {
163    base_url: Mutex<String>,
164    #[deref]
165    client: HttpClientWithProxy,
166}
167
168impl HttpClientWithUrl {
169    /// Returns a new [`HttpClientWithUrl`] with the given base URL.
170    pub fn new(
171        client: Arc<dyn HttpClient>,
172        base_url: impl Into<String>,
173        proxy_url: Option<String>,
174    ) -> Self {
175        let client = HttpClientWithProxy::new(client, proxy_url);
176
177        Self {
178            base_url: Mutex::new(base_url.into()),
179            client,
180        }
181    }
182
183    pub fn new_url(
184        client: Arc<dyn HttpClient>,
185        base_url: impl Into<String>,
186        proxy_url: Option<Url>,
187    ) -> Self {
188        let client = HttpClientWithProxy::new_url(client, proxy_url);
189
190        Self {
191            base_url: Mutex::new(base_url.into()),
192            client,
193        }
194    }
195
196    /// Returns the base URL.
197    pub fn base_url(&self) -> String {
198        self.base_url.lock().clone()
199    }
200
201    /// Sets the base URL.
202    pub fn set_base_url(&self, base_url: impl Into<String>) {
203        let base_url = base_url.into();
204        *self.base_url.lock() = base_url;
205    }
206
207    /// Builds a URL using the given path.
208    pub fn build_url(&self, path: &str) -> String {
209        format!("{}{}", self.base_url(), path)
210    }
211}
212
213impl HttpClient for HttpClientWithUrl {
214    fn send(
215        &self,
216        req: Request<AsyncBody>,
217    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
218        self.client.send(req)
219    }
220
221    fn user_agent(&self) -> Option<&HeaderValue> {
222        self.client.user_agent()
223    }
224
225    fn proxy(&self) -> Option<&Url> {
226        self.client.proxy.as_ref()
227    }
228
229    #[cfg(feature = "test-support")]
230    fn as_fake(&self) -> &FakeHttpClient {
231        self.client.as_fake()
232    }
233}
234
235pub fn read_proxy_from_env() -> Option<Url> {
236    const ENV_VARS: &[&str] = &[
237        "ALL_PROXY",
238        "all_proxy",
239        "HTTPS_PROXY",
240        "https_proxy",
241        "HTTP_PROXY",
242        "http_proxy",
243    ];
244
245    ENV_VARS
246        .iter()
247        .find_map(|var| std::env::var(var).ok())
248        .and_then(|env| env.parse().ok())
249}
250
251pub fn read_no_proxy_from_env() -> Option<String> {
252    const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
253
254    ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
255}
256
257pub struct BlockedHttpClient;
258
259impl BlockedHttpClient {
260    pub fn new() -> Self {
261        BlockedHttpClient
262    }
263}
264
265impl HttpClient for BlockedHttpClient {
266    fn send(
267        &self,
268        _req: Request<AsyncBody>,
269    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
270        Box::pin(async {
271            Err(std::io::Error::new(
272                std::io::ErrorKind::PermissionDenied,
273                "BlockedHttpClient disallowed request",
274            )
275            .into())
276        })
277    }
278
279    fn user_agent(&self) -> Option<&HeaderValue> {
280        None
281    }
282
283    fn proxy(&self) -> Option<&Url> {
284        None
285    }
286
287    #[cfg(feature = "test-support")]
288    fn as_fake(&self) -> &FakeHttpClient {
289        panic!("called as_fake on {}", type_name::<Self>())
290    }
291}
292
293#[cfg(feature = "test-support")]
294type FakeHttpHandler = Arc<
295    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
296        + Send
297        + Sync
298        + 'static,
299>;
300
301#[cfg(feature = "test-support")]
302pub struct FakeHttpClient {
303    handler: Mutex<Option<FakeHttpHandler>>,
304    user_agent: HeaderValue,
305}
306
307#[cfg(feature = "test-support")]
308impl FakeHttpClient {
309    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
310    where
311        Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
312        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
313    {
314        Arc::new(HttpClientWithUrl {
315            base_url: Mutex::new("http://test.example".into()),
316            client: HttpClientWithProxy {
317                client: Arc::new(Self {
318                    handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
319                    user_agent: HeaderValue::from_static(type_name::<Self>()),
320                }),
321                proxy: None,
322            },
323        })
324    }
325
326    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
327        log::warn!("Using fake HTTP client with 404 response");
328        Self::create(|_| async move {
329            Ok(Response::builder()
330                .status(404)
331                .body(Default::default())
332                .unwrap())
333        })
334    }
335
336    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
337        log::warn!("Using fake HTTP client with 200 response");
338        Self::create(|_| async move {
339            Ok(Response::builder()
340                .status(200)
341                .body(Default::default())
342                .unwrap())
343        })
344    }
345
346    pub fn replace_handler<Fut, F>(&self, new_handler: F)
347    where
348        Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
349        F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
350    {
351        let mut handler = self.handler.lock();
352        let old_handler = handler.take().unwrap();
353        *handler = Some(Arc::new(move |req| {
354            Box::pin(new_handler(old_handler.clone(), req))
355        }));
356    }
357}
358
359#[cfg(feature = "test-support")]
360impl fmt::Debug for FakeHttpClient {
361    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362        f.debug_struct("FakeHttpClient").finish()
363    }
364}
365
366#[cfg(feature = "test-support")]
367impl HttpClient for FakeHttpClient {
368    fn send(
369        &self,
370        req: Request<AsyncBody>,
371    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
372        ((self.handler.lock().as_ref().unwrap())(req)) as _
373    }
374
375    fn user_agent(&self) -> Option<&HeaderValue> {
376        Some(&self.user_agent)
377    }
378
379    fn proxy(&self) -> Option<&Url> {
380        None
381    }
382
383    fn as_fake(&self) -> &FakeHttpClient {
384        self
385    }
386}