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(
 75        &self,
 76        req: Request<AsyncBody>,
 77    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
 78        self.client.send(req)
 79    }
 80}
 81
 82impl HttpClient for HttpClientWithUrl {
 83    fn send(
 84        &self,
 85        req: Request<AsyncBody>,
 86    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
 87        self.client.send(req)
 88    }
 89}
 90
 91pub trait HttpClient: Send + Sync {
 92    fn send(
 93        &self,
 94        req: Request<AsyncBody>,
 95    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
 96
 97    fn get<'a>(
 98        &'a self,
 99        uri: &str,
100        body: AsyncBody,
101        follow_redirects: bool,
102    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
103        let request = isahc::Request::builder()
104            .redirect_policy(if follow_redirects {
105                RedirectPolicy::Follow
106            } else {
107                RedirectPolicy::None
108            })
109            .method(Method::GET)
110            .uri(uri)
111            .body(body);
112        match request {
113            Ok(request) => self.send(request),
114            Err(error) => async move { Err(error.into()) }.boxed(),
115        }
116    }
117
118    fn post_json<'a>(
119        &'a self,
120        uri: &str,
121        body: AsyncBody,
122    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
123        let request = isahc::Request::builder()
124            .method(Method::POST)
125            .uri(uri)
126            .header("Content-Type", "application/json")
127            .body(body);
128        match request {
129            Ok(request) => self.send(request),
130            Err(error) => async move { Err(error.into()) }.boxed(),
131        }
132    }
133}
134
135pub fn client() -> Arc<dyn HttpClient> {
136    Arc::new(
137        isahc::HttpClient::builder()
138            .connect_timeout(Duration::from_secs(5))
139            .low_speed_timeout(100, Duration::from_secs(5))
140            .proxy(http_proxy_from_env())
141            .build()
142            .unwrap(),
143    )
144}
145
146impl HttpClient for isahc::HttpClient {
147    fn send(
148        &self,
149        req: Request<AsyncBody>,
150    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
151        let client = self.clone();
152        Box::pin(async move { client.send_async(req).await })
153    }
154}
155
156#[cfg(feature = "test-support")]
157type FakeHttpHandler = Box<
158    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
159        + Send
160        + Sync
161        + 'static,
162>;
163
164#[cfg(feature = "test-support")]
165pub struct FakeHttpClient {
166    handler: FakeHttpHandler,
167}
168
169#[cfg(feature = "test-support")]
170impl FakeHttpClient {
171    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
172    where
173        Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
174        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
175    {
176        Arc::new(HttpClientWithUrl {
177            base_url: Mutex::new("http://test.example".into()),
178            client: Arc::new(Self {
179                handler: Box::new(move |req| Box::pin(handler(req))),
180            }),
181        })
182    }
183
184    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
185        Self::create(|_| async move {
186            Ok(Response::builder()
187                .status(404)
188                .body(Default::default())
189                .unwrap())
190        })
191    }
192
193    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
194        Self::create(|_| async move {
195            Ok(Response::builder()
196                .status(200)
197                .body(Default::default())
198                .unwrap())
199        })
200    }
201}
202
203#[cfg(feature = "test-support")]
204impl fmt::Debug for FakeHttpClient {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        f.debug_struct("FakeHttpClient").finish()
207    }
208}
209
210#[cfg(feature = "test-support")]
211impl HttpClient for FakeHttpClient {
212    fn send(
213        &self,
214        req: Request<AsyncBody>,
215    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
216        let future = (self.handler)(req);
217        Box::pin(async move { future.await.map(Into::into) })
218    }
219}