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(user_agent: Option<String>, 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(user_agent, 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(
128        base_url: impl Into<String>,
129        user_agent: Option<String>,
130        proxy_url: Option<String>,
131    ) -> Self {
132        let client = HttpClientWithProxy::new(user_agent, proxy_url);
133
134        Self {
135            base_url: Mutex::new(base_url.into()),
136            client,
137        }
138    }
139
140    /// Returns the base URL.
141    pub fn base_url(&self) -> String {
142        self.base_url
143            .lock()
144            .map_or_else(|_| Default::default(), |url| url.clone())
145    }
146
147    /// Sets the base URL.
148    pub fn set_base_url(&self, base_url: impl Into<String>) {
149        let base_url = base_url.into();
150        self.base_url
151            .lock()
152            .map(|mut url| {
153                *url = base_url;
154            })
155            .ok();
156    }
157
158    /// Builds a URL using the given path.
159    pub fn build_url(&self, path: &str) -> String {
160        format!("{}{}", self.base_url(), path)
161    }
162
163    /// Builds a Zed API URL using the given path.
164    pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
165        let base_url = self.base_url();
166        let base_api_url = match base_url.as_ref() {
167            "https://zed.dev" => "https://api.zed.dev",
168            "https://staging.zed.dev" => "https://api-staging.zed.dev",
169            "http://localhost:3000" => "http://localhost:8080",
170            other => other,
171        };
172
173        Ok(Url::parse_with_params(
174            &format!("{}{}", base_api_url, path),
175            query,
176        )?)
177    }
178}
179
180impl HttpClient for Arc<HttpClientWithUrl> {
181    fn send(
182        &self,
183        req: Request<AsyncBody>,
184    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
185        self.client.send(req)
186    }
187
188    fn proxy(&self) -> Option<&Uri> {
189        self.client.proxy.as_ref()
190    }
191}
192
193impl HttpClient for HttpClientWithUrl {
194    fn send(
195        &self,
196        req: Request<AsyncBody>,
197    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
198        self.client.send(req)
199    }
200
201    fn proxy(&self) -> Option<&Uri> {
202        self.client.proxy.as_ref()
203    }
204}
205
206pub fn client(user_agent: Option<String>, proxy: Option<Uri>) -> Arc<dyn HttpClient> {
207    let mut builder = isahc::HttpClient::builder()
208        .connect_timeout(Duration::from_secs(5))
209        .low_speed_timeout(100, Duration::from_secs(5))
210        .proxy(proxy.clone());
211    if let Some(user_agent) = user_agent {
212        builder = builder.default_header("User-Agent", user_agent);
213    }
214
215    Arc::new(HttpClientWithProxy {
216        client: Arc::new(builder.build().unwrap()),
217        proxy,
218    })
219}
220
221fn read_proxy_from_env() -> Option<Uri> {
222    const ENV_VARS: &[&str] = &[
223        "ALL_PROXY",
224        "all_proxy",
225        "HTTPS_PROXY",
226        "https_proxy",
227        "HTTP_PROXY",
228        "http_proxy",
229    ];
230
231    for var in ENV_VARS {
232        if let Ok(env) = std::env::var(var) {
233            return env.parse::<Uri>().ok();
234        }
235    }
236
237    None
238}
239
240impl HttpClient for isahc::HttpClient {
241    fn send(
242        &self,
243        req: Request<AsyncBody>,
244    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
245        let client = self.clone();
246        Box::pin(async move { client.send_async(req).await })
247    }
248
249    fn proxy(&self) -> Option<&Uri> {
250        None
251    }
252}
253
254#[cfg(feature = "test-support")]
255type FakeHttpHandler = Box<
256    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
257        + Send
258        + Sync
259        + 'static,
260>;
261
262#[cfg(feature = "test-support")]
263pub struct FakeHttpClient {
264    handler: FakeHttpHandler,
265}
266
267#[cfg(feature = "test-support")]
268impl FakeHttpClient {
269    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
270    where
271        Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
272        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
273    {
274        Arc::new(HttpClientWithUrl {
275            base_url: Mutex::new("http://test.example".into()),
276            client: HttpClientWithProxy {
277                client: Arc::new(Self {
278                    handler: Box::new(move |req| Box::pin(handler(req))),
279                }),
280                proxy: None,
281            },
282        })
283    }
284
285    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
286        Self::create(|_| async move {
287            Ok(Response::builder()
288                .status(404)
289                .body(Default::default())
290                .unwrap())
291        })
292    }
293
294    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
295        Self::create(|_| async move {
296            Ok(Response::builder()
297                .status(200)
298                .body(Default::default())
299                .unwrap())
300        })
301    }
302}
303
304#[cfg(feature = "test-support")]
305impl fmt::Debug for FakeHttpClient {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        f.debug_struct("FakeHttpClient").finish()
308    }
309}
310
311#[cfg(feature = "test-support")]
312impl HttpClient for FakeHttpClient {
313    fn send(
314        &self,
315        req: Request<AsyncBody>,
316    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
317        let future = (self.handler)(req);
318        Box::pin(async move { future.await.map(Into::into) })
319    }
320
321    fn proxy(&self) -> Option<&Uri> {
322        None
323    }
324}