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