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 LLM URL using the given path.
240    pub fn build_zed_llm_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://llm-staging.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
256impl HttpClient for Arc<HttpClientWithUrl> {
257    fn send(
258        &self,
259        req: Request<AsyncBody>,
260    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
261        self.client.send(req)
262    }
263
264    fn user_agent(&self) -> Option<&HeaderValue> {
265        self.client.user_agent()
266    }
267
268    fn proxy(&self) -> Option<&Url> {
269        self.client.proxy.as_ref()
270    }
271
272    fn type_name(&self) -> &'static str {
273        self.client.type_name()
274    }
275}
276
277impl HttpClient for HttpClientWithUrl {
278    fn send(
279        &self,
280        req: Request<AsyncBody>,
281    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
282        self.client.send(req)
283    }
284
285    fn user_agent(&self) -> Option<&HeaderValue> {
286        self.client.user_agent()
287    }
288
289    fn proxy(&self) -> Option<&Url> {
290        self.client.proxy.as_ref()
291    }
292
293    fn type_name(&self) -> &'static str {
294        self.client.type_name()
295    }
296}
297
298pub fn read_proxy_from_env() -> Option<Url> {
299    const ENV_VARS: &[&str] = &[
300        "ALL_PROXY",
301        "all_proxy",
302        "HTTPS_PROXY",
303        "https_proxy",
304        "HTTP_PROXY",
305        "http_proxy",
306    ];
307
308    ENV_VARS
309        .iter()
310        .find_map(|var| std::env::var(var).ok())
311        .and_then(|env| env.parse().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    fn type_name(&self) -> &'static str {
345        type_name::<Self>()
346    }
347}
348
349#[cfg(feature = "test-support")]
350type FakeHttpHandler = Box<
351    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
352        + Send
353        + Sync
354        + 'static,
355>;
356
357#[cfg(feature = "test-support")]
358pub struct FakeHttpClient {
359    handler: FakeHttpHandler,
360    user_agent: HeaderValue,
361}
362
363#[cfg(feature = "test-support")]
364impl FakeHttpClient {
365    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
366    where
367        Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
368        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
369    {
370        Arc::new(HttpClientWithUrl {
371            base_url: Mutex::new("http://test.example".into()),
372            client: HttpClientWithProxy {
373                client: Arc::new(Self {
374                    handler: Box::new(move |req| Box::pin(handler(req))),
375                    user_agent: HeaderValue::from_static(type_name::<Self>()),
376                }),
377                proxy: None,
378            },
379        })
380    }
381
382    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
383        Self::create(|_| async move {
384            Ok(Response::builder()
385                .status(404)
386                .body(Default::default())
387                .unwrap())
388        })
389    }
390
391    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
392        Self::create(|_| async move {
393            Ok(Response::builder()
394                .status(200)
395                .body(Default::default())
396                .unwrap())
397        })
398    }
399}
400
401#[cfg(feature = "test-support")]
402impl fmt::Debug for FakeHttpClient {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        f.debug_struct("FakeHttpClient").finish()
405    }
406}
407
408#[cfg(feature = "test-support")]
409impl HttpClient for FakeHttpClient {
410    fn send(
411        &self,
412        req: Request<AsyncBody>,
413    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
414        let future = (self.handler)(req);
415        future
416    }
417
418    fn user_agent(&self) -> Option<&HeaderValue> {
419        Some(&self.user_agent)
420    }
421
422    fn proxy(&self) -> Option<&Url> {
423        None
424    }
425
426    fn type_name(&self) -> &'static str {
427        type_name::<Self>()
428    }
429}