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 http_proxy_from_env() -> 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    try_env!(
 31        "ALL_PROXY",
 32        "all_proxy",
 33        "HTTPS_PROXY",
 34        "https_proxy",
 35        "HTTP_PROXY",
 36        "http_proxy"
 37    );
 38    None
 39}
 40
 41/// An [`HttpClient`] that has a base URL.
 42pub struct HttpClientWithUrl {
 43    base_url: Mutex<String>,
 44    client: Arc<dyn HttpClient>,
 45}
 46
 47impl HttpClientWithUrl {
 48    /// Returns a new [`HttpClientWithUrl`] with the given base URL.
 49    pub fn new(base_url: impl Into<String>) -> Self {
 50        Self {
 51            base_url: Mutex::new(base_url.into()),
 52            client: client(),
 53        }
 54    }
 55
 56    /// Returns the base URL.
 57    pub fn base_url(&self) -> String {
 58        self.base_url
 59            .lock()
 60            .map_or_else(|_| Default::default(), |url| url.clone())
 61    }
 62
 63    /// Sets the base URL.
 64    pub fn set_base_url(&self, base_url: impl Into<String>) {
 65        let base_url = base_url.into();
 66        self.base_url
 67            .lock()
 68            .map(|mut url| {
 69                *url = base_url;
 70            })
 71            .ok();
 72    }
 73
 74    /// Builds a URL using the given path.
 75    pub fn build_url(&self, path: &str) -> String {
 76        format!("{}{}", self.base_url(), path)
 77    }
 78
 79    /// Builds a Zed API URL using the given path.
 80    pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
 81        let base_url = self.base_url();
 82        let base_api_url = match base_url.as_ref() {
 83            "https://zed.dev" => "https://api.zed.dev",
 84            "https://staging.zed.dev" => "https://api-staging.zed.dev",
 85            "http://localhost:3000" => "http://localhost:8080",
 86            other => other,
 87        };
 88
 89        Ok(Url::parse_with_params(
 90            &format!("{}{}", base_api_url, path),
 91            query,
 92        )?)
 93    }
 94}
 95
 96impl HttpClient for Arc<HttpClientWithUrl> {
 97    fn send(
 98        &self,
 99        req: Request<AsyncBody>,
100    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
101        self.client.send(req)
102    }
103}
104
105impl HttpClient for HttpClientWithUrl {
106    fn send(
107        &self,
108        req: Request<AsyncBody>,
109    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
110        self.client.send(req)
111    }
112}
113
114pub trait HttpClient: Send + Sync {
115    fn send(
116        &self,
117        req: Request<AsyncBody>,
118    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
119
120    fn get<'a>(
121        &'a self,
122        uri: &str,
123        body: AsyncBody,
124        follow_redirects: bool,
125    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
126        let request = isahc::Request::builder()
127            .redirect_policy(if follow_redirects {
128                RedirectPolicy::Follow
129            } else {
130                RedirectPolicy::None
131            })
132            .method(Method::GET)
133            .uri(uri)
134            .body(body);
135        match request {
136            Ok(request) => self.send(request),
137            Err(error) => async move { Err(error.into()) }.boxed(),
138        }
139    }
140
141    fn post_json<'a>(
142        &'a self,
143        uri: &str,
144        body: AsyncBody,
145    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
146        let request = isahc::Request::builder()
147            .method(Method::POST)
148            .uri(uri)
149            .header("Content-Type", "application/json")
150            .body(body);
151        match request {
152            Ok(request) => self.send(request),
153            Err(error) => async move { Err(error.into()) }.boxed(),
154        }
155    }
156}
157
158pub fn client() -> Arc<dyn HttpClient> {
159    Arc::new(
160        isahc::HttpClient::builder()
161            .connect_timeout(Duration::from_secs(5))
162            .low_speed_timeout(100, Duration::from_secs(5))
163            .proxy(http_proxy_from_env())
164            .build()
165            .unwrap(),
166    )
167}
168
169impl HttpClient for isahc::HttpClient {
170    fn send(
171        &self,
172        req: Request<AsyncBody>,
173    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
174        let client = self.clone();
175        Box::pin(async move { client.send_async(req).await })
176    }
177}
178
179#[cfg(feature = "test-support")]
180type FakeHttpHandler = Box<
181    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
182        + Send
183        + Sync
184        + 'static,
185>;
186
187#[cfg(feature = "test-support")]
188pub struct FakeHttpClient {
189    handler: FakeHttpHandler,
190}
191
192#[cfg(feature = "test-support")]
193impl FakeHttpClient {
194    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
195    where
196        Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
197        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
198    {
199        Arc::new(HttpClientWithUrl {
200            base_url: Mutex::new("http://test.example".into()),
201            client: Arc::new(Self {
202                handler: Box::new(move |req| Box::pin(handler(req))),
203            }),
204        })
205    }
206
207    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
208        Self::create(|_| async move {
209            Ok(Response::builder()
210                .status(404)
211                .body(Default::default())
212                .unwrap())
213        })
214    }
215
216    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
217        Self::create(|_| async move {
218            Ok(Response::builder()
219                .status(200)
220                .body(Default::default())
221                .unwrap())
222        })
223    }
224}
225
226#[cfg(feature = "test-support")]
227impl fmt::Debug for FakeHttpClient {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        f.debug_struct("FakeHttpClient").finish()
230    }
231}
232
233#[cfg(feature = "test-support")]
234impl HttpClient for FakeHttpClient {
235    fn send(
236        &self,
237        req: Request<AsyncBody>,
238    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
239        let future = (self.handler)(req);
240        Box::pin(async move { future.await.map(Into::into) })
241    }
242}