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