http_client.rs

  1pub mod github;
  2
  3pub use anyhow::{anyhow, Result};
  4use derive_more::Deref;
  5use futures::future::BoxFuture;
  6use futures_lite::FutureExt;
  7use isahc::config::{Configurable, RedirectPolicy};
  8pub use isahc::{
  9    http::{Method, StatusCode, Uri},
 10    AsyncBody, Error, HttpClient as IsahcHttpClient, Request, Response,
 11};
 12#[cfg(feature = "test-support")]
 13use std::fmt;
 14use std::{
 15    sync::{Arc, Mutex},
 16    time::Duration,
 17};
 18pub use url::Url;
 19
 20pub trait HttpClient: Send + Sync {
 21    fn send(
 22        &self,
 23        req: Request<AsyncBody>,
 24    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
 25
 26    fn get<'a>(
 27        &'a self,
 28        uri: &str,
 29        body: AsyncBody,
 30        follow_redirects: bool,
 31    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
 32        let request = isahc::Request::builder()
 33            .redirect_policy(if follow_redirects {
 34                RedirectPolicy::Follow
 35            } else {
 36                RedirectPolicy::None
 37            })
 38            .method(Method::GET)
 39            .uri(uri)
 40            .body(body);
 41        match request {
 42            Ok(request) => self.send(request),
 43            Err(error) => async move { Err(error.into()) }.boxed(),
 44        }
 45    }
 46
 47    fn post_json<'a>(
 48        &'a self,
 49        uri: &str,
 50        body: AsyncBody,
 51    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
 52        let request = isahc::Request::builder()
 53            .method(Method::POST)
 54            .uri(uri)
 55            .header("Content-Type", "application/json")
 56            .body(body);
 57        match request {
 58            Ok(request) => self.send(request),
 59            Err(error) => async move { Err(error.into()) }.boxed(),
 60        }
 61    }
 62
 63    fn proxy(&self) -> Option<&Uri>;
 64}
 65
 66/// An [`HttpClient`] that may have a proxy.
 67#[derive(Deref)]
 68pub struct HttpClientWithProxy {
 69    #[deref]
 70    client: Arc<dyn HttpClient>,
 71    proxy: Option<Uri>,
 72}
 73
 74impl HttpClientWithProxy {
 75    /// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
 76    pub fn new(proxy_url: Option<String>) -> Self {
 77        let proxy_url = proxy_url
 78            .and_then(|input| {
 79                input
 80                    .parse::<Uri>()
 81                    .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
 82                    .ok()
 83            })
 84            .or_else(read_proxy_from_env);
 85
 86        Self {
 87            client: client(proxy_url.clone()),
 88            proxy: proxy_url,
 89        }
 90    }
 91}
 92
 93impl HttpClient for HttpClientWithProxy {
 94    fn send(
 95        &self,
 96        req: Request<AsyncBody>,
 97    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
 98        self.client.send(req)
 99    }
100
101    fn proxy(&self) -> Option<&Uri> {
102        self.proxy.as_ref()
103    }
104}
105
106impl HttpClient for Arc<HttpClientWithProxy> {
107    fn send(
108        &self,
109        req: Request<AsyncBody>,
110    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
111        self.client.send(req)
112    }
113
114    fn proxy(&self) -> Option<&Uri> {
115        self.proxy.as_ref()
116    }
117}
118
119/// An [`HttpClient`] that has a base URL.
120pub struct HttpClientWithUrl {
121    base_url: Mutex<String>,
122    client: HttpClientWithProxy,
123}
124
125impl HttpClientWithUrl {
126    /// Returns a new [`HttpClientWithUrl`] with the given base URL.
127    pub fn new(base_url: impl Into<String>, proxy_url: Option<String>) -> Self {
128        let client = HttpClientWithProxy::new(proxy_url);
129
130        Self {
131            base_url: Mutex::new(base_url.into()),
132            client,
133        }
134    }
135
136    /// Returns the base URL.
137    pub fn base_url(&self) -> String {
138        self.base_url
139            .lock()
140            .map_or_else(|_| Default::default(), |url| url.clone())
141    }
142
143    /// Sets the base URL.
144    pub fn set_base_url(&self, base_url: impl Into<String>) {
145        let base_url = base_url.into();
146        self.base_url
147            .lock()
148            .map(|mut url| {
149                *url = base_url;
150            })
151            .ok();
152    }
153
154    /// Builds a URL using the given path.
155    pub fn build_url(&self, path: &str) -> String {
156        format!("{}{}", self.base_url(), path)
157    }
158
159    /// Builds a Zed API URL using the given path.
160    pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
161        let base_url = self.base_url();
162        let base_api_url = match base_url.as_ref() {
163            "https://zed.dev" => "https://api.zed.dev",
164            "https://staging.zed.dev" => "https://api-staging.zed.dev",
165            "http://localhost:3000" => "http://localhost:8080",
166            other => other,
167        };
168
169        Ok(Url::parse_with_params(
170            &format!("{}{}", base_api_url, path),
171            query,
172        )?)
173    }
174}
175
176impl HttpClient for Arc<HttpClientWithUrl> {
177    fn send(
178        &self,
179        req: Request<AsyncBody>,
180    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
181        self.client.send(req)
182    }
183
184    fn proxy(&self) -> Option<&Uri> {
185        self.client.proxy.as_ref()
186    }
187}
188
189impl HttpClient for HttpClientWithUrl {
190    fn send(
191        &self,
192        req: Request<AsyncBody>,
193    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
194        self.client.send(req)
195    }
196
197    fn proxy(&self) -> Option<&Uri> {
198        self.client.proxy.as_ref()
199    }
200}
201
202pub fn client(proxy: Option<Uri>) -> Arc<dyn HttpClient> {
203    Arc::new(HttpClientWithProxy {
204        client: Arc::new(
205            isahc::HttpClient::builder()
206                .connect_timeout(Duration::from_secs(5))
207                .low_speed_timeout(100, Duration::from_secs(5))
208                .proxy(proxy.clone())
209                .build()
210                .unwrap(),
211        ),
212        proxy,
213    })
214}
215
216fn read_proxy_from_env() -> Option<Uri> {
217    const ENV_VARS: &[&str] = &[
218        "ALL_PROXY",
219        "all_proxy",
220        "HTTPS_PROXY",
221        "https_proxy",
222        "HTTP_PROXY",
223        "http_proxy",
224    ];
225
226    for var in ENV_VARS {
227        if let Ok(env) = std::env::var(var) {
228            return env.parse::<Uri>().ok();
229        }
230    }
231
232    None
233}
234
235impl HttpClient for isahc::HttpClient {
236    fn send(
237        &self,
238        req: Request<AsyncBody>,
239    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
240        let client = self.clone();
241        Box::pin(async move { client.send_async(req).await })
242    }
243
244    fn proxy(&self) -> Option<&Uri> {
245        None
246    }
247}
248
249#[cfg(feature = "test-support")]
250type FakeHttpHandler = Box<
251    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
252        + Send
253        + Sync
254        + 'static,
255>;
256
257#[cfg(feature = "test-support")]
258pub struct FakeHttpClient {
259    handler: FakeHttpHandler,
260}
261
262#[cfg(feature = "test-support")]
263impl FakeHttpClient {
264    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
265    where
266        Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
267        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
268    {
269        Arc::new(HttpClientWithUrl {
270            base_url: Mutex::new("http://test.example".into()),
271            client: HttpClientWithProxy {
272                client: Arc::new(Self {
273                    handler: Box::new(move |req| Box::pin(handler(req))),
274                }),
275                proxy: None,
276            },
277        })
278    }
279
280    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
281        Self::create(|_| async move {
282            Ok(Response::builder()
283                .status(404)
284                .body(Default::default())
285                .unwrap())
286        })
287    }
288
289    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
290        Self::create(|_| async move {
291            Ok(Response::builder()
292                .status(200)
293                .body(Default::default())
294                .unwrap())
295        })
296    }
297}
298
299#[cfg(feature = "test-support")]
300impl fmt::Debug for FakeHttpClient {
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        f.debug_struct("FakeHttpClient").finish()
303    }
304}
305
306#[cfg(feature = "test-support")]
307impl HttpClient for FakeHttpClient {
308    fn send(
309        &self,
310        req: Request<AsyncBody>,
311    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
312        let future = (self.handler)(req);
313        Box::pin(async move { future.await.map(Into::into) })
314    }
315
316    fn proxy(&self) -> Option<&Uri> {
317        None
318    }
319}