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