http.rs

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