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