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