http_client.rs

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