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