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};
  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}