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        // Some requests to Qwen2 models on Runpod can take 32+ seconds,
225        // especially if there's a cold boot involved. We may need to have
226        // those requests use a different http client, because global timeouts
227        // of 50 and 60 seconds, respectively, would be very high!
228        .connect_timeout(Duration::from_secs(5))
229        .low_speed_timeout(100, Duration::from_secs(5))
230        .proxy(proxy.clone());
231    if let Some(user_agent) = user_agent {
232        builder = builder.default_header("User-Agent", user_agent);
233    }
234
235    Arc::new(HttpClientWithProxy {
236        client: Arc::new(builder.build().unwrap()),
237        proxy,
238    })
239}
240
241fn read_proxy_from_env() -> Option<Uri> {
242    const ENV_VARS: &[&str] = &[
243        "ALL_PROXY",
244        "all_proxy",
245        "HTTPS_PROXY",
246        "https_proxy",
247        "HTTP_PROXY",
248        "http_proxy",
249    ];
250
251    for var in ENV_VARS {
252        if let Ok(env) = std::env::var(var) {
253            return env.parse::<Uri>().ok();
254        }
255    }
256
257    None
258}
259
260impl HttpClient for isahc::HttpClient {
261    fn send(
262        &self,
263        req: Request<AsyncBody>,
264    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
265        let client = self.clone();
266        Box::pin(async move { client.send_async(req).await })
267    }
268
269    fn proxy(&self) -> Option<&Uri> {
270        None
271    }
272}
273
274#[cfg(feature = "test-support")]
275type FakeHttpHandler = Box<
276    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
277        + Send
278        + Sync
279        + 'static,
280>;
281
282#[cfg(feature = "test-support")]
283pub struct FakeHttpClient {
284    handler: FakeHttpHandler,
285}
286
287#[cfg(feature = "test-support")]
288impl FakeHttpClient {
289    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
290    where
291        Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
292        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
293    {
294        Arc::new(HttpClientWithUrl {
295            base_url: Mutex::new("http://test.example".into()),
296            client: HttpClientWithProxy {
297                client: Arc::new(Self {
298                    handler: Box::new(move |req| Box::pin(handler(req))),
299                }),
300                proxy: None,
301            },
302        })
303    }
304
305    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
306        Self::create(|_| async move {
307            Ok(Response::builder()
308                .status(404)
309                .body(Default::default())
310                .unwrap())
311        })
312    }
313
314    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
315        Self::create(|_| async move {
316            Ok(Response::builder()
317                .status(200)
318                .body(Default::default())
319                .unwrap())
320        })
321    }
322}
323
324#[cfg(feature = "test-support")]
325impl fmt::Debug for FakeHttpClient {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        f.debug_struct("FakeHttpClient").finish()
328    }
329}
330
331#[cfg(feature = "test-support")]
332impl HttpClient for FakeHttpClient {
333    fn send(
334        &self,
335        req: Request<AsyncBody>,
336    ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
337        let future = (self.handler)(req);
338        Box::pin(async move { future.await.map(Into::into) })
339    }
340
341    fn proxy(&self) -> Option<&Uri> {
342        None
343    }
344}