http_client.rs

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