Skip to main content

open_gpui_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;
10pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
11use http::{HeaderName, HeaderValue};
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
59/// A set of pre-validated user-supplied HTTP headers.
60///
61/// Construction (and the per-name validation that goes with it) happens once
62/// at settings load time. Cloning is `Arc`-cheap, so providers can hand a copy
63/// to each outgoing request without re-parsing or re-allocating.
64#[derive(Default, Clone, Debug)]
65pub struct CustomHeaders(Arc<[(HeaderName, HeaderValue)]>);
66
67impl CustomHeaders {
68    pub fn new(headers: Vec<(HeaderName, HeaderValue)>) -> Self {
69        Self(headers.into())
70    }
71
72    pub fn is_empty(&self) -> bool {
73        self.0.is_empty()
74    }
75
76    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&HeaderName, &HeaderValue)> {
77        self.0.iter().map(|(n, v)| (n, v))
78    }
79}
80
81impl PartialEq for CustomHeaders {
82    fn eq(&self, other: &Self) -> bool {
83        self.0.len() == other.0.len()
84            && self
85                .0
86                .iter()
87                .zip(other.0.iter())
88                .all(|(a, b)| a.0 == b.0 && a.1 == b.1)
89    }
90}
91
92pub trait RequestBuilderExt {
93    /// Append every header in `headers` to the request being built.
94    fn extra_headers(self, headers: &CustomHeaders) -> Self;
95}
96
97impl RequestBuilderExt for http::request::Builder {
98    fn extra_headers(mut self, headers: &CustomHeaders) -> Self {
99        if headers.is_empty() {
100            return self;
101        }
102        if let Some(map) = self.headers_mut() {
103            for (name, value) in headers.iter() {
104                map.append(name.clone(), value.clone());
105            }
106        }
107        self
108    }
109}
110
111pub trait HttpClient: 'static + Send + Sync {
112    fn user_agent(&self) -> Option<&HeaderValue>;
113
114    fn proxy(&self) -> Option<&Url>;
115
116    fn send(
117        &self,
118        req: http::Request<AsyncBody>,
119    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
120
121    fn get(
122        &self,
123        uri: &str,
124        body: AsyncBody,
125        follow_redirects: bool,
126    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
127        let request = Builder::new()
128            .uri(uri)
129            .follow_redirects(if follow_redirects {
130                RedirectPolicy::FollowAll
131            } else {
132                RedirectPolicy::NoFollow
133            })
134            .body(body);
135
136        match request {
137            Ok(request) => self.send(request),
138            Err(e) => Box::pin(async move { Err(e.into()) }),
139        }
140    }
141
142    fn post_json(
143        &self,
144        uri: &str,
145        body: AsyncBody,
146    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
147        let request = Builder::new()
148            .uri(uri)
149            .method(Method::POST)
150            .header("Content-Type", "application/json")
151            .body(body);
152
153        match request {
154            Ok(request) => self.send(request),
155            Err(e) => Box::pin(async move { Err(e.into()) }),
156        }
157    }
158
159    #[cfg(feature = "test-support")]
160    fn as_fake(&self) -> &FakeHttpClient {
161        panic!("called as_fake on {}", type_name::<Self>())
162    }
163}
164
165/// An [`HttpClient`] that may have a proxy.
166#[derive(Deref)]
167pub struct HttpClientWithProxy {
168    #[deref]
169    client: Arc<dyn HttpClient>,
170    proxy: Option<Url>,
171}
172
173impl HttpClientWithProxy {
174    /// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
175    pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
176        let proxy_url = proxy_url
177            .and_then(|proxy| proxy.parse().ok())
178            .or_else(read_proxy_from_env);
179
180        Self::new_url(client, proxy_url)
181    }
182    pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
183        Self {
184            client,
185            proxy: proxy_url,
186        }
187    }
188}
189
190impl HttpClient for HttpClientWithProxy {
191    fn send(
192        &self,
193        req: Request<AsyncBody>,
194    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
195        self.client.send(req)
196    }
197
198    fn user_agent(&self) -> Option<&HeaderValue> {
199        self.client.user_agent()
200    }
201
202    fn proxy(&self) -> Option<&Url> {
203        self.proxy.as_ref()
204    }
205
206    #[cfg(feature = "test-support")]
207    fn as_fake(&self) -> &FakeHttpClient {
208        self.client.as_fake()
209    }
210}
211
212/// An [`HttpClient`] that has a base URL.
213#[derive(Deref)]
214pub struct HttpClientWithUrl {
215    base_url: Mutex<String>,
216    #[deref]
217    client: HttpClientWithProxy,
218}
219
220impl HttpClientWithUrl {
221    /// Returns a new [`HttpClientWithUrl`] with the given base URL.
222    pub fn new(
223        client: Arc<dyn HttpClient>,
224        base_url: impl Into<String>,
225        proxy_url: Option<String>,
226    ) -> Self {
227        let client = HttpClientWithProxy::new(client, proxy_url);
228
229        Self {
230            base_url: Mutex::new(base_url.into()),
231            client,
232        }
233    }
234
235    pub fn new_url(
236        client: Arc<dyn HttpClient>,
237        base_url: impl Into<String>,
238        proxy_url: Option<Url>,
239    ) -> Self {
240        let client = HttpClientWithProxy::new_url(client, proxy_url);
241
242        Self {
243            base_url: Mutex::new(base_url.into()),
244            client,
245        }
246    }
247
248    /// Returns the base URL.
249    pub fn base_url(&self) -> String {
250        self.base_url.lock().clone()
251    }
252
253    /// Sets the base URL.
254    pub fn set_base_url(&self, base_url: impl Into<String>) {
255        let base_url = base_url.into();
256        *self.base_url.lock() = base_url;
257    }
258
259    /// Builds a URL using the given path.
260    pub fn build_url(&self, path: &str) -> String {
261        format!("{}{}", self.base_url(), path)
262    }
263}
264
265impl HttpClient for HttpClientWithUrl {
266    fn send(
267        &self,
268        req: Request<AsyncBody>,
269    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
270        self.client.send(req)
271    }
272
273    fn user_agent(&self) -> Option<&HeaderValue> {
274        self.client.user_agent()
275    }
276
277    fn proxy(&self) -> Option<&Url> {
278        self.client.proxy.as_ref()
279    }
280
281    #[cfg(feature = "test-support")]
282    fn as_fake(&self) -> &FakeHttpClient {
283        self.client.as_fake()
284    }
285}
286
287pub fn read_proxy_from_env() -> Option<Url> {
288    const ENV_VARS: &[&str] = &[
289        "ALL_PROXY",
290        "all_proxy",
291        "HTTPS_PROXY",
292        "https_proxy",
293        "HTTP_PROXY",
294        "http_proxy",
295    ];
296
297    ENV_VARS
298        .iter()
299        .find_map(|var| std::env::var(var).ok())
300        .and_then(|env| env.parse().ok())
301}
302
303pub fn read_no_proxy_from_env() -> Option<String> {
304    const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
305
306    ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
307}
308
309pub struct BlockedHttpClient;
310
311impl BlockedHttpClient {
312    pub fn new() -> Self {
313        BlockedHttpClient
314    }
315}
316
317impl HttpClient for BlockedHttpClient {
318    fn send(
319        &self,
320        _req: Request<AsyncBody>,
321    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
322        Box::pin(async {
323            Err(std::io::Error::new(
324                std::io::ErrorKind::PermissionDenied,
325                "BlockedHttpClient disallowed request",
326            )
327            .into())
328        })
329    }
330
331    fn user_agent(&self) -> Option<&HeaderValue> {
332        None
333    }
334
335    fn proxy(&self) -> Option<&Url> {
336        None
337    }
338
339    #[cfg(feature = "test-support")]
340    fn as_fake(&self) -> &FakeHttpClient {
341        panic!("called as_fake on {}", type_name::<Self>())
342    }
343}
344
345#[cfg(feature = "test-support")]
346type FakeHttpHandler = Arc<
347    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
348        + Send
349        + Sync
350        + 'static,
351>;
352
353#[cfg(feature = "test-support")]
354pub struct FakeHttpClient {
355    handler: Mutex<Option<FakeHttpHandler>>,
356    user_agent: HeaderValue,
357}
358
359#[cfg(feature = "test-support")]
360impl FakeHttpClient {
361    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
362    where
363        Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
364        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
365    {
366        Arc::new(HttpClientWithUrl {
367            base_url: Mutex::new("http://test.example".into()),
368            client: HttpClientWithProxy {
369                client: Arc::new(Self {
370                    handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
371                    user_agent: HeaderValue::from_static(type_name::<Self>()),
372                }),
373                proxy: None,
374            },
375        })
376    }
377
378    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
379        log::warn!("Using fake HTTP client with 404 response");
380        Self::create(|_| async move {
381            Ok(Response::builder()
382                .status(404)
383                .body(Default::default())
384                .unwrap())
385        })
386    }
387
388    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
389        log::warn!("Using fake HTTP client with 200 response");
390        Self::create(|_| async move {
391            Ok(Response::builder()
392                .status(200)
393                .body(Default::default())
394                .unwrap())
395        })
396    }
397
398    pub fn replace_handler<Fut, F>(&self, new_handler: F)
399    where
400        Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
401        F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
402    {
403        let mut handler = self.handler.lock();
404        let old_handler = handler.take().unwrap();
405        *handler = Some(Arc::new(move |req| {
406            Box::pin(new_handler(old_handler.clone(), req))
407        }));
408    }
409}
410
411#[cfg(feature = "test-support")]
412impl fmt::Debug for FakeHttpClient {
413    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
414        f.debug_struct("FakeHttpClient").finish()
415    }
416}
417
418#[cfg(feature = "test-support")]
419impl HttpClient for FakeHttpClient {
420    fn send(
421        &self,
422        req: Request<AsyncBody>,
423    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
424        ((self.handler.lock().as_ref().unwrap())(req)) as _
425    }
426
427    fn user_agent(&self) -> Option<&HeaderValue> {
428        Some(&self.user_agent)
429    }
430
431    fn proxy(&self) -> Option<&Url> {
432        None
433    }
434
435    fn as_fake(&self) -> &FakeHttpClient {
436        self
437    }
438}