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