http.rs

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