Skip to main content

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