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::{
 12    FutureExt as _,
 13    future::{self, BoxFuture},
 14};
 15use parking_lot::Mutex;
 16#[cfg(feature = "test-support")]
 17use std::fmt;
 18use std::{any::type_name, sync::Arc};
 19pub use url::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 type_name(&self) -> &'static str;
 62
 63    fn user_agent(&self) -> Option<&HeaderValue>;
 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    fn proxy(&self) -> Option<&Url>;
109
110    #[cfg(feature = "test-support")]
111    fn as_fake(&self) -> &FakeHttpClient {
112        panic!("called as_fake on {}", type_name::<Self>())
113    }
114
115    fn send_multipart_form<'a>(
116        &'a self,
117        _url: &str,
118        _request: reqwest::multipart::Form,
119    ) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
120        future::ready(Err(anyhow!("not implemented"))).boxed()
121    }
122}
123
124/// An [`HttpClient`] that may have a proxy.
125#[derive(Deref)]
126pub struct HttpClientWithProxy {
127    #[deref]
128    client: Arc<dyn HttpClient>,
129    proxy: Option<Url>,
130}
131
132impl HttpClientWithProxy {
133    /// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
134    pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
135        let proxy_url = proxy_url
136            .and_then(|proxy| proxy.parse().ok())
137            .or_else(read_proxy_from_env);
138
139        Self::new_url(client, proxy_url)
140    }
141    pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
142        Self {
143            client,
144            proxy: proxy_url,
145        }
146    }
147}
148
149impl HttpClient for HttpClientWithProxy {
150    fn send(
151        &self,
152        req: Request<AsyncBody>,
153    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
154        self.client.send(req)
155    }
156
157    fn user_agent(&self) -> Option<&HeaderValue> {
158        self.client.user_agent()
159    }
160
161    fn proxy(&self) -> Option<&Url> {
162        self.proxy.as_ref()
163    }
164
165    fn type_name(&self) -> &'static str {
166        self.client.type_name()
167    }
168
169    #[cfg(feature = "test-support")]
170    fn as_fake(&self) -> &FakeHttpClient {
171        self.client.as_fake()
172    }
173
174    fn send_multipart_form<'a>(
175        &'a self,
176        url: &str,
177        form: reqwest::multipart::Form,
178    ) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
179        self.client.send_multipart_form(url, form)
180    }
181}
182
183/// An [`HttpClient`] that has a base URL.
184pub struct HttpClientWithUrl {
185    base_url: Mutex<String>,
186    client: HttpClientWithProxy,
187}
188
189impl std::ops::Deref for HttpClientWithUrl {
190    type Target = HttpClientWithProxy;
191
192    fn deref(&self) -> &Self::Target {
193        &self.client
194    }
195}
196
197impl HttpClientWithUrl {
198    /// Returns a new [`HttpClientWithUrl`] with the given base URL.
199    pub fn new(
200        client: Arc<dyn HttpClient>,
201        base_url: impl Into<String>,
202        proxy_url: Option<String>,
203    ) -> Self {
204        let client = HttpClientWithProxy::new(client, proxy_url);
205
206        Self {
207            base_url: Mutex::new(base_url.into()),
208            client,
209        }
210    }
211
212    pub fn new_url(
213        client: Arc<dyn HttpClient>,
214        base_url: impl Into<String>,
215        proxy_url: Option<Url>,
216    ) -> Self {
217        let client = HttpClientWithProxy::new_url(client, proxy_url);
218
219        Self {
220            base_url: Mutex::new(base_url.into()),
221            client,
222        }
223    }
224
225    /// Returns the base URL.
226    pub fn base_url(&self) -> String {
227        self.base_url.lock().clone()
228    }
229
230    /// Sets the base URL.
231    pub fn set_base_url(&self, base_url: impl Into<String>) {
232        let base_url = base_url.into();
233        *self.base_url.lock() = base_url;
234    }
235
236    /// Builds a URL using the given path.
237    pub fn build_url(&self, path: &str) -> String {
238        format!("{}{}", self.base_url(), path)
239    }
240
241    /// Builds a Zed API URL using the given path.
242    pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
243        let base_url = self.base_url();
244        let base_api_url = match base_url.as_ref() {
245            "https://zed.dev" => "https://api.zed.dev",
246            "https://staging.zed.dev" => "https://api-staging.zed.dev",
247            "http://localhost:3000" => "http://localhost:8080",
248            other => other,
249        };
250
251        Ok(Url::parse_with_params(
252            &format!("{}{}", base_api_url, path),
253            query,
254        )?)
255    }
256
257    /// Builds a Zed Cloud URL using the given path.
258    pub fn build_zed_cloud_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
259        let base_url = self.base_url();
260        let base_api_url = match base_url.as_ref() {
261            "https://zed.dev" => "https://cloud.zed.dev",
262            "https://staging.zed.dev" => "https://cloud.zed.dev",
263            "http://localhost:3000" => "http://localhost:8787",
264            other => other,
265        };
266
267        Ok(Url::parse_with_params(
268            &format!("{}{}", base_api_url, path),
269            query,
270        )?)
271    }
272
273    /// Builds a Zed LLM URL using the given path.
274    pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
275        let base_url = self.base_url();
276        let base_api_url = match base_url.as_ref() {
277            "https://zed.dev" => "https://cloud.zed.dev",
278            "https://staging.zed.dev" => "https://llm-staging.zed.dev",
279            "http://localhost:3000" => "http://localhost:8787",
280            other => other,
281        };
282
283        Ok(Url::parse_with_params(
284            &format!("{}{}", base_api_url, path),
285            query,
286        )?)
287    }
288}
289
290impl HttpClient for HttpClientWithUrl {
291    fn send(
292        &self,
293        req: Request<AsyncBody>,
294    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
295        self.client.send(req)
296    }
297
298    fn user_agent(&self) -> Option<&HeaderValue> {
299        self.client.user_agent()
300    }
301
302    fn proxy(&self) -> Option<&Url> {
303        self.client.proxy.as_ref()
304    }
305
306    fn type_name(&self) -> &'static str {
307        self.client.type_name()
308    }
309
310    #[cfg(feature = "test-support")]
311    fn as_fake(&self) -> &FakeHttpClient {
312        self.client.as_fake()
313    }
314
315    fn send_multipart_form<'a>(
316        &'a self,
317        url: &str,
318        request: reqwest::multipart::Form,
319    ) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
320        self.client.send_multipart_form(url, request)
321    }
322}
323
324pub fn read_proxy_from_env() -> Option<Url> {
325    const ENV_VARS: &[&str] = &[
326        "ALL_PROXY",
327        "all_proxy",
328        "HTTPS_PROXY",
329        "https_proxy",
330        "HTTP_PROXY",
331        "http_proxy",
332    ];
333
334    ENV_VARS
335        .iter()
336        .find_map(|var| std::env::var(var).ok())
337        .and_then(|env| env.parse().ok())
338}
339
340pub fn read_no_proxy_from_env() -> Option<String> {
341    const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
342
343    ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
344}
345
346pub struct BlockedHttpClient;
347
348impl BlockedHttpClient {
349    pub const 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        ((self.handler.lock().as_ref().unwrap())(req)) as _
464    }
465
466    fn user_agent(&self) -> Option<&HeaderValue> {
467        Some(&self.user_agent)
468    }
469
470    fn proxy(&self) -> Option<&Url> {
471        None
472    }
473
474    fn type_name(&self) -> &'static str {
475        type_name::<Self>()
476    }
477
478    fn as_fake(&self) -> &FakeHttpClient {
479        self
480    }
481}