http_client.rs

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