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")]
132type FakeHttpHandler = Box<
133    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
134        + Send
135        + Sync
136        + 'static,
137>;
138
139#[cfg(feature = "test-support")]
140pub struct FakeHttpClient {
141    handler: FakeHttpHandler,
142}
143
144#[cfg(feature = "test-support")]
145impl FakeHttpClient {
146    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
147    where
148        Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
149        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
150    {
151        Arc::new(HttpClientWithUrl {
152            base_url: Mutex::new("http://test.example".into()),
153            client: Arc::new(Self {
154                handler: Box::new(move |req| Box::pin(handler(req))),
155            }),
156        })
157    }
158
159    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
160        Self::create(|_| async move {
161            Ok(Response::builder()
162                .status(404)
163                .body(Default::default())
164                .unwrap())
165        })
166    }
167
168    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
169        Self::create(|_| async move {
170            Ok(Response::builder()
171                .status(200)
172                .body(Default::default())
173                .unwrap())
174        })
175    }
176}
177
178#[cfg(feature = "test-support")]
179impl fmt::Debug for FakeHttpClient {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        f.debug_struct("FakeHttpClient").finish()
182    }
183}
184
185#[cfg(feature = "test-support")]
186impl HttpClient for FakeHttpClient {
187    fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
188        let future = (self.handler)(req);
189        Box::pin(async move { future.await.map(Into::into) })
190    }
191}