Skip to main content

http_client/
http_client.rs

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