http_client.rs

  1mod async_body;
  2pub mod github;
  3
  4pub use anyhow::{Result, anyhow};
  5pub use async_body::{AsyncBody, Inner};
  6use derive_more::Deref;
  7pub use http::{self, Method, Request, Response, StatusCode, Uri};
  8
  9use futures::future::BoxFuture;
 10use http::request::Builder;
 11#[cfg(feature = "test-support")]
 12use std::fmt;
 13use std::{
 14    any::type_name,
 15    sync::{Arc, Mutex},
 16};
 17pub use url::Url;
 18
 19#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
 20pub enum RedirectPolicy {
 21    #[default]
 22    NoFollow,
 23    FollowLimit(u32),
 24    FollowAll,
 25}
 26pub struct FollowRedirects(pub bool);
 27
 28pub trait HttpRequestExt {
 29    /// Whether or not to follow redirects
 30    fn follow_redirects(self, follow: RedirectPolicy) -> Self;
 31}
 32
 33impl HttpRequestExt for http::request::Builder {
 34    fn follow_redirects(self, follow: RedirectPolicy) -> Self {
 35        self.extension(follow)
 36    }
 37}
 38
 39pub trait HttpClient: 'static + Send + Sync {
 40    fn type_name(&self) -> &'static str;
 41
 42    fn send(
 43        &self,
 44        req: http::Request<AsyncBody>,
 45    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
 46
 47    fn get<'a>(
 48        &'a self,
 49        uri: &str,
 50        body: AsyncBody,
 51        follow_redirects: bool,
 52    ) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
 53        let request = Builder::new()
 54            .uri(uri)
 55            .follow_redirects(if follow_redirects {
 56                RedirectPolicy::FollowAll
 57            } else {
 58                RedirectPolicy::NoFollow
 59            })
 60            .body(body);
 61
 62        match request {
 63            Ok(request) => Box::pin(async move { self.send(request).await }),
 64            Err(e) => Box::pin(async move { Err(e.into()) }),
 65        }
 66    }
 67
 68    fn post_json<'a>(
 69        &'a self,
 70        uri: &str,
 71        body: AsyncBody,
 72    ) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
 73        let request = Builder::new()
 74            .uri(uri)
 75            .method(Method::POST)
 76            .header("Content-Type", "application/json")
 77            .body(body);
 78
 79        match request {
 80            Ok(request) => Box::pin(async move { self.send(request).await }),
 81            Err(e) => Box::pin(async move { Err(e.into()) }),
 82        }
 83    }
 84
 85    fn proxy(&self) -> Option<&Url>;
 86}
 87
 88/// An [`HttpClient`] that may have a proxy.
 89#[derive(Deref)]
 90pub struct HttpClientWithProxy {
 91    #[deref]
 92    client: Arc<dyn HttpClient>,
 93    proxy: Option<Url>,
 94}
 95
 96impl HttpClientWithProxy {
 97    /// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
 98    pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
 99        let proxy_url = proxy_url
100            .and_then(|proxy| proxy.parse().ok())
101            .or_else(read_proxy_from_env);
102
103        Self::new_url(client, proxy_url)
104    }
105    pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
106        Self {
107            client,
108            proxy: proxy_url,
109        }
110    }
111}
112
113impl HttpClient for HttpClientWithProxy {
114    fn send(
115        &self,
116        req: Request<AsyncBody>,
117    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
118        self.client.send(req)
119    }
120
121    fn proxy(&self) -> Option<&Url> {
122        self.proxy.as_ref()
123    }
124
125    fn type_name(&self) -> &'static str {
126        self.client.type_name()
127    }
128}
129
130impl HttpClient for Arc<HttpClientWithProxy> {
131    fn send(
132        &self,
133        req: Request<AsyncBody>,
134    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
135        self.client.send(req)
136    }
137
138    fn proxy(&self) -> Option<&Url> {
139        self.proxy.as_ref()
140    }
141
142    fn type_name(&self) -> &'static str {
143        self.client.type_name()
144    }
145}
146
147/// An [`HttpClient`] that has a base URL.
148pub struct HttpClientWithUrl {
149    base_url: Mutex<String>,
150    client: HttpClientWithProxy,
151}
152
153impl std::ops::Deref for HttpClientWithUrl {
154    type Target = HttpClientWithProxy;
155
156    fn deref(&self) -> &Self::Target {
157        &self.client
158    }
159}
160
161impl HttpClientWithUrl {
162    /// Returns a new [`HttpClientWithUrl`] with the given base URL.
163    pub fn new(
164        client: Arc<dyn HttpClient>,
165        base_url: impl Into<String>,
166        proxy_url: Option<String>,
167    ) -> Self {
168        let client = HttpClientWithProxy::new(client, proxy_url);
169
170        Self {
171            base_url: Mutex::new(base_url.into()),
172            client,
173        }
174    }
175
176    pub fn new_url(
177        client: Arc<dyn HttpClient>,
178        base_url: impl Into<String>,
179        proxy_url: Option<Url>,
180    ) -> Self {
181        let client = HttpClientWithProxy::new_url(client, proxy_url);
182
183        Self {
184            base_url: Mutex::new(base_url.into()),
185            client,
186        }
187    }
188
189    /// Returns the base URL.
190    pub fn base_url(&self) -> String {
191        self.base_url
192            .lock()
193            .map_or_else(|_| Default::default(), |url| url.clone())
194    }
195
196    /// Sets the base URL.
197    pub fn set_base_url(&self, base_url: impl Into<String>) {
198        let base_url = base_url.into();
199        self.base_url
200            .lock()
201            .map(|mut url| {
202                *url = base_url;
203            })
204            .ok();
205    }
206
207    /// Builds a URL using the given path.
208    pub fn build_url(&self, path: &str) -> String {
209        format!("{}{}", self.base_url(), path)
210    }
211
212    /// Builds a Zed API URL using the given path.
213    pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
214        let base_url = self.base_url();
215        let base_api_url = match base_url.as_ref() {
216            "https://zed.dev" => "https://api.zed.dev",
217            "https://staging.zed.dev" => "https://api-staging.zed.dev",
218            "http://localhost:3000" => "http://localhost:8080",
219            other => other,
220        };
221
222        Ok(Url::parse_with_params(
223            &format!("{}{}", base_api_url, path),
224            query,
225        )?)
226    }
227
228    /// Builds a Zed LLM URL using the given path.
229    pub fn build_zed_llm_url(
230        &self,
231        path: &str,
232        query: &[(&str, &str)],
233        use_cloud: bool,
234    ) -> Result<Url> {
235        let base_url = self.base_url();
236        let base_api_url = match base_url.as_ref() {
237            "https://zed.dev" => {
238                if use_cloud {
239                    "https://cloud.zed.dev"
240                } else {
241                    "https://llm.zed.dev"
242                }
243            }
244            "https://staging.zed.dev" => "https://llm-staging.zed.dev",
245            "http://localhost:3000" => "http://localhost:8787",
246            other => other,
247        };
248
249        Ok(Url::parse_with_params(
250            &format!("{}{}", base_api_url, path),
251            query,
252        )?)
253    }
254}
255
256impl HttpClient for Arc<HttpClientWithUrl> {
257    fn send(
258        &self,
259        req: Request<AsyncBody>,
260    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
261        self.client.send(req)
262    }
263
264    fn proxy(&self) -> Option<&Url> {
265        self.client.proxy.as_ref()
266    }
267
268    fn type_name(&self) -> &'static str {
269        self.client.type_name()
270    }
271}
272
273impl HttpClient for HttpClientWithUrl {
274    fn send(
275        &self,
276        req: Request<AsyncBody>,
277    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
278        self.client.send(req)
279    }
280
281    fn proxy(&self) -> Option<&Url> {
282        self.client.proxy.as_ref()
283    }
284
285    fn type_name(&self) -> &'static str {
286        self.client.type_name()
287    }
288}
289
290pub fn read_proxy_from_env() -> Option<Url> {
291    const ENV_VARS: &[&str] = &[
292        "ALL_PROXY",
293        "all_proxy",
294        "HTTPS_PROXY",
295        "https_proxy",
296        "HTTP_PROXY",
297        "http_proxy",
298    ];
299
300    ENV_VARS
301        .iter()
302        .find_map(|var| std::env::var(var).ok())
303        .and_then(|env| env.parse().ok())
304}
305
306pub struct BlockedHttpClient;
307
308impl BlockedHttpClient {
309    pub fn new() -> Self {
310        BlockedHttpClient
311    }
312}
313
314impl HttpClient for BlockedHttpClient {
315    fn send(
316        &self,
317        _req: Request<AsyncBody>,
318    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
319        Box::pin(async {
320            Err(std::io::Error::new(
321                std::io::ErrorKind::PermissionDenied,
322                "BlockedHttpClient disallowed request",
323            )
324            .into())
325        })
326    }
327
328    fn proxy(&self) -> Option<&Url> {
329        None
330    }
331
332    fn type_name(&self) -> &'static str {
333        type_name::<Self>()
334    }
335}
336
337#[cfg(feature = "test-support")]
338type FakeHttpHandler = Box<
339    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
340        + Send
341        + Sync
342        + 'static,
343>;
344
345#[cfg(feature = "test-support")]
346pub struct FakeHttpClient {
347    handler: FakeHttpHandler,
348}
349
350#[cfg(feature = "test-support")]
351impl FakeHttpClient {
352    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
353    where
354        Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
355        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
356    {
357        Arc::new(HttpClientWithUrl {
358            base_url: Mutex::new("http://test.example".into()),
359            client: HttpClientWithProxy {
360                client: Arc::new(Self {
361                    handler: Box::new(move |req| Box::pin(handler(req))),
362                }),
363                proxy: None,
364            },
365        })
366    }
367
368    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
369        Self::create(|_| async move {
370            Ok(Response::builder()
371                .status(404)
372                .body(Default::default())
373                .unwrap())
374        })
375    }
376
377    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
378        Self::create(|_| async move {
379            Ok(Response::builder()
380                .status(200)
381                .body(Default::default())
382                .unwrap())
383        })
384    }
385}
386
387#[cfg(feature = "test-support")]
388impl fmt::Debug for FakeHttpClient {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        f.debug_struct("FakeHttpClient").finish()
391    }
392}
393
394#[cfg(feature = "test-support")]
395impl HttpClient for FakeHttpClient {
396    fn send(
397        &self,
398        req: Request<AsyncBody>,
399    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
400        let future = (self.handler)(req);
401        future
402    }
403
404    fn proxy(&self) -> Option<&Url> {
405        None
406    }
407
408    fn type_name(&self) -> &'static str {
409        type_name::<Self>()
410    }
411}