http.rs

  1use crate::http_proxy_from_env;
  2pub use anyhow::{anyhow, Result};
  3use futures::future::BoxFuture;
  4use futures_lite::FutureExt;
  5use isahc::config::{Configurable, RedirectPolicy};
  6pub use isahc::{
  7    http::{Method, StatusCode, Uri},
  8    AsyncBody, Error, HttpClient as IsahcHttpClient, Request, Response,
  9};
 10#[cfg(feature = "test-support")]
 11use std::fmt;
 12use std::{
 13    sync::{Arc, Mutex},
 14    time::Duration,
 15};
 16pub use url::Url;
 17
 18/// An [`HttpClient`] that has a base URL.
 19pub struct HttpClientWithUrl {
 20    base_url: Mutex<String>,
 21    client: Arc<dyn HttpClient>,
 22}
 23
 24impl HttpClientWithUrl {
 25    /// Returns a new [`HttpClientWithUrl`] with the given base URL.
 26    pub fn new(base_url: impl Into<String>) -> Self {
 27        Self {
 28            base_url: Mutex::new(base_url.into()),
 29            client: client(),
 30        }
 31    }
 32
 33    /// Returns the base URL.
 34    pub fn base_url(&self) -> String {
 35        self.base_url
 36            .lock()
 37            .map_or_else(|_| Default::default(), |url| url.clone())
 38    }
 39
 40    /// Sets the base URL.
 41    pub fn set_base_url(&self, base_url: impl Into<String>) {
 42        let base_url = base_url.into();
 43        self.base_url
 44            .lock()
 45            .map(|mut url| {
 46                *url = base_url;
 47            })
 48            .ok();
 49    }
 50
 51    /// Builds a URL using the given path.
 52    pub fn build_url(&self, path: &str) -> String {
 53        format!("{}{}", self.base_url(), path)
 54    }
 55
 56    /// Builds a Zed API URL using the given path.
 57    pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
 58        let base_url = self.base_url();
 59        let base_api_url = match base_url.as_ref() {
 60            "https://zed.dev" => "https://api.zed.dev",
 61            "https://staging.zed.dev" => "https://api-staging.zed.dev",
 62            "http://localhost:3000" => "http://localhost:8080",
 63            other => other,
 64        };
 65
 66        Ok(Url::parse_with_params(
 67            &format!("{}{}", base_api_url, path),
 68            query,
 69        )?)
 70    }
 71}
 72
 73impl HttpClient for Arc<HttpClientWithUrl> {
 74    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
 75        self.client.send(req)
 76    }
 77}
 78
 79impl HttpClient for HttpClientWithUrl {
 80    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
 81        self.client.send(req)
 82    }
 83}
 84
 85pub trait HttpClient: Send + Sync {
 86    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>>;
 87
 88    fn get<'a>(
 89        &'a self,
 90        uri: &str,
 91        body: AsyncBody,
 92        follow_redirects: bool,
 93    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
 94        let request = isahc::Request::builder()
 95            .redirect_policy(if follow_redirects {
 96                RedirectPolicy::Follow
 97            } else {
 98                RedirectPolicy::None
 99            })
100            .method(Method::GET)
101            .uri(uri)
102            .body(body);
103        match request {
104            Ok(request) => self.send(request),
105            Err(error) => async move { Err(error.into()) }.boxed(),
106        }
107    }
108
109    fn post_json<'a>(
110        &'a self,
111        uri: &str,
112        body: AsyncBody,
113    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
114        let request = isahc::Request::builder()
115            .method(Method::POST)
116            .uri(uri)
117            .header("Content-Type", "application/json")
118            .body(body);
119        match request {
120            Ok(request) => self.send(request),
121            Err(error) => async move { Err(error.into()) }.boxed(),
122        }
123    }
124}
125
126pub fn client() -> Arc<dyn HttpClient> {
127    Arc::new(
128        isahc::HttpClient::builder()
129            .connect_timeout(Duration::from_secs(5))
130            .low_speed_timeout(100, Duration::from_secs(5))
131            .proxy(http_proxy_from_env())
132            .build()
133            .unwrap(),
134    )
135}
136
137impl HttpClient for isahc::HttpClient {
138    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
139        Box::pin(async move { self.send_async(req).await })
140    }
141}
142
143#[cfg(feature = "test-support")]
144type FakeHttpHandler = Box<
145    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
146        + Send
147        + Sync
148        + 'static,
149>;
150
151#[cfg(feature = "test-support")]
152pub struct FakeHttpClient {
153    handler: FakeHttpHandler,
154}
155
156#[cfg(feature = "test-support")]
157impl FakeHttpClient {
158    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
159    where
160        Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
161        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
162    {
163        Arc::new(HttpClientWithUrl {
164            base_url: Mutex::new("http://test.example".into()),
165            client: Arc::new(Self {
166                handler: Box::new(move |req| Box::pin(handler(req))),
167            }),
168        })
169    }
170
171    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
172        Self::create(|_| async move {
173            Ok(Response::builder()
174                .status(404)
175                .body(Default::default())
176                .unwrap())
177        })
178    }
179
180    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
181        Self::create(|_| async move {
182            Ok(Response::builder()
183                .status(200)
184                .body(Default::default())
185                .unwrap())
186        })
187    }
188}
189
190#[cfg(feature = "test-support")]
191impl fmt::Debug for FakeHttpClient {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        f.debug_struct("FakeHttpClient").finish()
194    }
195}
196
197#[cfg(feature = "test-support")]
198impl HttpClient for FakeHttpClient {
199    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
200        let future = (self.handler)(req);
201        Box::pin(async move { future.await.map(Into::into) })
202    }
203}