http_client.rs

  1pub mod github;
  2
  3pub use anyhow::{anyhow, Result};
  4use derive_more::Deref;
  5use futures::future::BoxFuture;
  6use futures_lite::FutureExt;
  7use isahc::config::{Configurable, RedirectPolicy};
  8pub use isahc::{
  9    http::{Method, StatusCode, Uri},
 10    AsyncBody, Error, HttpClient as IsahcHttpClient, Request, Response,
 11};
 12#[cfg(feature = "test-support")]
 13use std::fmt;
 14use std::{
 15    sync::{Arc, Mutex},
 16    time::Duration,
 17};
 18pub use url::Url;
 19
 20pub trait HttpClient: Send + Sync {
 21    fn send(
 22        &self,
 23        req: Request<AsyncBody>,
 24    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
 25
 26    fn get<'a>(
 27        &'a self,
 28        uri: &str,
 29        body: AsyncBody,
 30        follow_redirects: bool,
 31    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
 32        let request = isahc::Request::builder()
 33            .redirect_policy(if follow_redirects {
 34                RedirectPolicy::Follow
 35            } else {
 36                RedirectPolicy::None
 37            })
 38            .method(Method::GET)
 39            .uri(uri)
 40            .body(body);
 41        match request {
 42            Ok(request) => self.send(request),
 43            Err(error) => async move { Err(error.into()) }.boxed(),
 44        }
 45    }
 46
 47    fn post_json<'a>(
 48        &'a self,
 49        uri: &str,
 50        body: AsyncBody,
 51    ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
 52        let request = isahc::Request::builder()
 53            .method(Method::POST)
 54            .uri(uri)
 55            .header("Content-Type", "application/json")
 56            .body(body);
 57        match request {
 58            Ok(request) => self.send(request),
 59            Err(error) => async move { Err(error.into()) }.boxed(),
 60        }
 61    }
 62
 63    fn proxy(&self) -> Option<&Uri>;
 64}
 65
 66/// An [`HttpClient`] that may have a proxy.
 67#[derive(Deref)]
 68pub struct HttpClientWithProxy {
 69    #[deref]
 70    client: Arc<dyn HttpClient>,
 71    proxy: Option<Uri>,
 72}
 73
 74impl HttpClientWithProxy {
 75    /// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
 76    pub fn new(user_agent: Option<String>, proxy_url: Option<String>) -> Self {
 77        let proxy_url = proxy_url
 78            .and_then(|input| {
 79                input
 80                    .parse::<Uri>()
 81                    .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
 82                    .ok()
 83            })
 84            .or_else(read_proxy_from_env);
 85
 86        Self {
 87            client: client(user_agent, proxy_url.clone()),
 88            proxy: proxy_url,
 89        }
 90    }
 91}
 92
 93impl HttpClient for HttpClientWithProxy {
 94    fn send(
 95        &self,
 96        req: Request<AsyncBody>,
 97    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
 98        self.client.send(req)
 99    }
100
101    fn proxy(&self) -> Option<&Uri> {
102        self.proxy.as_ref()
103    }
104}
105
106impl HttpClient for Arc<HttpClientWithProxy> {
107    fn send(
108        &self,
109        req: Request<AsyncBody>,
110    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
111        self.client.send(req)
112    }
113
114    fn proxy(&self) -> Option<&Uri> {
115        self.proxy.as_ref()
116    }
117}
118
119/// An [`HttpClient`] that has a base URL.
120pub struct HttpClientWithUrl {
121    base_url: Mutex<String>,
122    client: HttpClientWithProxy,
123}
124
125impl HttpClientWithUrl {
126    /// Returns a new [`HttpClientWithUrl`] with the given base URL.
127    pub fn new(
128        base_url: impl Into<String>,
129        user_agent: Option<String>,
130        proxy_url: Option<String>,
131    ) -> Self {
132        let client = HttpClientWithProxy::new(user_agent, proxy_url);
133
134        Self {
135            base_url: Mutex::new(base_url.into()),
136            client,
137        }
138    }
139
140    /// Returns the base URL.
141    pub fn base_url(&self) -> String {
142        self.base_url
143            .lock()
144            .map_or_else(|_| Default::default(), |url| url.clone())
145    }
146
147    /// Sets the base URL.
148    pub fn set_base_url(&self, base_url: impl Into<String>) {
149        let base_url = base_url.into();
150        self.base_url
151            .lock()
152            .map(|mut url| {
153                *url = base_url;
154            })
155            .ok();
156    }
157
158    /// Builds a URL using the given path.
159    pub fn build_url(&self, path: &str) -> String {
160        format!("{}{}", self.base_url(), path)
161    }
162
163    /// Builds a Zed API URL using the given path.
164    pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
165        let base_url = self.base_url();
166        let base_api_url = match base_url.as_ref() {
167            "https://zed.dev" => "https://api.zed.dev",
168            "https://staging.zed.dev" => "https://api-staging.zed.dev",
169            "http://localhost:3000" => "http://localhost:8080",
170            other => other,
171        };
172
173        Ok(Url::parse_with_params(
174            &format!("{}{}", base_api_url, path),
175            query,
176        )?)
177    }
178
179    /// Builds a Zed LLM URL using the given path.
180    pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
181        let base_url = self.base_url();
182        let base_api_url = match base_url.as_ref() {
183            "https://zed.dev" => "https://llm.zed.dev",
184            "https://staging.zed.dev" => "https://llm-staging.zed.dev",
185            "http://localhost:3000" => "http://localhost:8080",
186            other => other,
187        };
188
189        Ok(Url::parse_with_params(
190            &format!("{}{}", base_api_url, path),
191            query,
192        )?)
193    }
194}
195
196impl HttpClient for Arc<HttpClientWithUrl> {
197    fn send(
198        &self,
199        req: Request<AsyncBody>,
200    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
201        self.client.send(req)
202    }
203
204    fn proxy(&self) -> Option<&Uri> {
205        self.client.proxy.as_ref()
206    }
207}
208
209impl HttpClient for HttpClientWithUrl {
210    fn send(
211        &self,
212        req: Request<AsyncBody>,
213    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
214        self.client.send(req)
215    }
216
217    fn proxy(&self) -> Option<&Uri> {
218        self.client.proxy.as_ref()
219    }
220}
221
222pub fn client(user_agent: Option<String>, proxy: Option<Uri>) -> Arc<dyn HttpClient> {
223    let mut builder = isahc::HttpClient::builder()
224        .connect_timeout(Duration::from_secs(5))
225        .low_speed_timeout(100, Duration::from_secs(5))
226        .proxy(proxy.clone());
227    if let Some(user_agent) = user_agent {
228        builder = builder.default_header("User-Agent", user_agent);
229    }
230
231    Arc::new(HttpClientWithProxy {
232        client: Arc::new(builder.build().unwrap()),
233        proxy,
234    })
235}
236
237fn read_proxy_from_env() -> Option<Uri> {
238    const ENV_VARS: &[&str] = &[
239        "ALL_PROXY",
240        "all_proxy",
241        "HTTPS_PROXY",
242        "https_proxy",
243        "HTTP_PROXY",
244        "http_proxy",
245    ];
246
247    for var in ENV_VARS {
248        if let Ok(env) = std::env::var(var) {
249            return env.parse::<Uri>().ok();
250        }
251    }
252
253    None
254}
255
256impl HttpClient for isahc::HttpClient {
257    fn send(
258        &self,
259        req: Request<AsyncBody>,
260    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
261        let client = self.clone();
262        Box::pin(async move { client.send_async(req).await })
263    }
264
265    fn proxy(&self) -> Option<&Uri> {
266        None
267    }
268}
269
270#[cfg(feature = "test-support")]
271type FakeHttpHandler = Box<
272    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
273        + Send
274        + Sync
275        + 'static,
276>;
277
278#[cfg(feature = "test-support")]
279pub struct FakeHttpClient {
280    handler: FakeHttpHandler,
281}
282
283#[cfg(feature = "test-support")]
284impl FakeHttpClient {
285    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
286    where
287        Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
288        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
289    {
290        Arc::new(HttpClientWithUrl {
291            base_url: Mutex::new("http://test.example".into()),
292            client: HttpClientWithProxy {
293                client: Arc::new(Self {
294                    handler: Box::new(move |req| Box::pin(handler(req))),
295                }),
296                proxy: None,
297            },
298        })
299    }
300
301    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
302        Self::create(|_| async move {
303            Ok(Response::builder()
304                .status(404)
305                .body(Default::default())
306                .unwrap())
307        })
308    }
309
310    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
311        Self::create(|_| async move {
312            Ok(Response::builder()
313                .status(200)
314                .body(Default::default())
315                .unwrap())
316        })
317    }
318}
319
320#[cfg(feature = "test-support")]
321impl fmt::Debug for FakeHttpClient {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        f.debug_struct("FakeHttpClient").finish()
324    }
325}
326
327#[cfg(feature = "test-support")]
328impl HttpClient for FakeHttpClient {
329    fn send(
330        &self,
331        req: Request<AsyncBody>,
332    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
333        let future = (self.handler)(req);
334        Box::pin(async move { future.await.map(Into::into) })
335    }
336
337    fn proxy(&self) -> Option<&Uri> {
338        None
339    }
340}