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