Skip to main content

http_client/
http_client.rs

1mod async_body;
2#[cfg(not(target_family = "wasm"))]
3pub mod github;
4#[cfg(all(not(target_family = "wasm"), feature = "github-download"))]
5pub mod github_download;
6
7pub use anyhow::{Result, anyhow};
8pub use async_body::{AsyncBody, Inner, Json};
9use derive_more::Deref;
10pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
11use http::{HeaderName, HeaderValue};
12
13use futures::future::BoxFuture;
14use parking_lot::Mutex;
15use serde::Serialize;
16#[cfg(feature = "test-support")]
17use std::{any::type_name, fmt};
18use std::{sync::Arc, time::Duration};
19pub use url::{Host, 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
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct RequestTimeout(pub Duration);
32
33pub trait HttpRequestExt {
34    /// Conditionally modify self with the given closure.
35    fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
36    where
37        Self: Sized,
38    {
39        if condition { then(self) } else { self }
40    }
41
42    /// Conditionally unwrap and modify self with the given closure, if the given option is Some.
43    fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
44    where
45        Self: Sized,
46    {
47        match option {
48            Some(value) => then(self, value),
49            None => self,
50        }
51    }
52
53    /// Whether or not to follow redirects
54    fn follow_redirects(self, follow: RedirectPolicy) -> Self;
55
56    /// Sets a deadline for the complete HTTP request, including its response body.
57    fn timeout(self, timeout: Duration) -> Self;
58}
59
60impl HttpRequestExt for http::request::Builder {
61    fn follow_redirects(self, follow: RedirectPolicy) -> Self {
62        self.extension(follow)
63    }
64
65    fn timeout(self, timeout: Duration) -> Self {
66        debug_assert!(!timeout.is_zero(), "timeout must be positive");
67        self.extension(RequestTimeout(timeout))
68    }
69}
70
71/// A set of pre-validated user-supplied HTTP headers.
72///
73/// Construction (and the per-name validation that goes with it) happens once
74/// at settings load time. Cloning is `Arc`-cheap, so providers can hand a copy
75/// to each outgoing request without re-parsing or re-allocating.
76#[derive(Default, Clone, Debug)]
77pub struct CustomHeaders(Arc<[(HeaderName, HeaderValue)]>);
78
79impl CustomHeaders {
80    pub fn new(headers: Vec<(HeaderName, HeaderValue)>) -> Self {
81        Self(headers.into())
82    }
83
84    pub fn is_empty(&self) -> bool {
85        self.0.is_empty()
86    }
87
88    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&HeaderName, &HeaderValue)> {
89        self.0.iter().map(|(n, v)| (n, v))
90    }
91}
92
93impl PartialEq for CustomHeaders {
94    fn eq(&self, other: &Self) -> bool {
95        self.0.len() == other.0.len()
96            && self
97                .0
98                .iter()
99                .zip(other.0.iter())
100                .all(|(a, b)| a.0 == b.0 && a.1 == b.1)
101    }
102}
103
104pub trait RequestBuilderExt {
105    /// Append every header in `headers` to the request being built.
106    fn extra_headers(self, headers: &CustomHeaders) -> Self;
107}
108
109impl RequestBuilderExt for http::request::Builder {
110    fn extra_headers(mut self, headers: &CustomHeaders) -> Self {
111        if headers.is_empty() {
112            return self;
113        }
114        if let Some(map) = self.headers_mut() {
115            for (name, value) in headers.iter() {
116                map.append(name.clone(), value.clone());
117            }
118        }
119        self
120    }
121}
122
123pub trait HttpClient: 'static + Send + Sync {
124    fn user_agent(&self) -> Option<&HeaderValue>;
125
126    fn proxy(&self) -> Option<&Url>;
127
128    fn send(
129        &self,
130        req: http::Request<AsyncBody>,
131    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
132
133    fn get(
134        &self,
135        uri: &str,
136        body: AsyncBody,
137        follow_redirects: bool,
138    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
139        let request = Builder::new()
140            .uri(uri)
141            .follow_redirects(if follow_redirects {
142                RedirectPolicy::FollowAll
143            } else {
144                RedirectPolicy::NoFollow
145            })
146            .body(body);
147
148        match request {
149            Ok(request) => self.send(request),
150            Err(e) => Box::pin(async move { Err(e.into()) }),
151        }
152    }
153
154    fn post_json(
155        &self,
156        uri: &str,
157        body: AsyncBody,
158    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
159        let request = Builder::new()
160            .uri(uri)
161            .method(Method::POST)
162            .header("Content-Type", "application/json")
163            .body(body);
164
165        match request {
166            Ok(request) => self.send(request),
167            Err(e) => Box::pin(async move { Err(e.into()) }),
168        }
169    }
170
171    #[cfg(feature = "test-support")]
172    fn as_fake(&self) -> &FakeHttpClient {
173        panic!("called as_fake on {}", type_name::<Self>())
174    }
175}
176
177/// An [`HttpClient`] that may have a proxy.
178#[derive(Deref)]
179pub struct HttpClientWithProxy {
180    #[deref]
181    client: Arc<dyn HttpClient>,
182    proxy: Option<Url>,
183}
184
185impl HttpClientWithProxy {
186    /// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
187    pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
188        let proxy_url = proxy_url
189            .and_then(|proxy| proxy.parse().ok())
190            .or_else(read_proxy_from_env);
191
192        Self::new_url(client, proxy_url)
193    }
194    pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
195        Self {
196            client,
197            proxy: proxy_url,
198        }
199    }
200}
201
202impl HttpClient for HttpClientWithProxy {
203    fn send(
204        &self,
205        req: Request<AsyncBody>,
206    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
207        self.client.send(req)
208    }
209
210    fn user_agent(&self) -> Option<&HeaderValue> {
211        self.client.user_agent()
212    }
213
214    fn proxy(&self) -> Option<&Url> {
215        self.proxy.as_ref()
216    }
217
218    #[cfg(feature = "test-support")]
219    fn as_fake(&self) -> &FakeHttpClient {
220        self.client.as_fake()
221    }
222}
223
224/// An [`HttpClient`] that has a base URL.
225#[derive(Deref)]
226pub struct HttpClientWithUrl {
227    base_url: Mutex<String>,
228    #[deref]
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 HttpClient for HttpClientWithUrl {
336    fn send(
337        &self,
338        req: Request<AsyncBody>,
339    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
340        self.client.send(req)
341    }
342
343    fn user_agent(&self) -> Option<&HeaderValue> {
344        self.client.user_agent()
345    }
346
347    fn proxy(&self) -> Option<&Url> {
348        self.client.proxy.as_ref()
349    }
350
351    #[cfg(feature = "test-support")]
352    fn as_fake(&self) -> &FakeHttpClient {
353        self.client.as_fake()
354    }
355}
356
357pub fn read_proxy_from_env() -> Option<Url> {
358    const ENV_VARS: &[&str] = &[
359        "ALL_PROXY",
360        "all_proxy",
361        "HTTPS_PROXY",
362        "https_proxy",
363        "HTTP_PROXY",
364        "http_proxy",
365    ];
366
367    ENV_VARS
368        .iter()
369        .find_map(|var| std::env::var(var).ok())
370        .and_then(|env| env.parse().ok())
371}
372
373pub fn read_no_proxy_from_env() -> Option<String> {
374    const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
375
376    ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
377}
378
379pub struct BlockedHttpClient;
380
381impl BlockedHttpClient {
382    pub fn new() -> Self {
383        BlockedHttpClient
384    }
385}
386
387impl HttpClient for BlockedHttpClient {
388    fn send(
389        &self,
390        _req: Request<AsyncBody>,
391    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
392        Box::pin(async {
393            Err(std::io::Error::new(
394                std::io::ErrorKind::PermissionDenied,
395                "BlockedHttpClient disallowed request",
396            )
397            .into())
398        })
399    }
400
401    fn user_agent(&self) -> Option<&HeaderValue> {
402        None
403    }
404
405    fn proxy(&self) -> Option<&Url> {
406        None
407    }
408
409    #[cfg(feature = "test-support")]
410    fn as_fake(&self) -> &FakeHttpClient {
411        panic!("called as_fake on {}", type_name::<Self>())
412    }
413}
414
415#[cfg(feature = "test-support")]
416type FakeHttpHandler = Arc<
417    dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
418        + Send
419        + Sync
420        + 'static,
421>;
422
423#[cfg(feature = "test-support")]
424pub struct FakeHttpClient {
425    handler: Mutex<Option<FakeHttpHandler>>,
426    user_agent: HeaderValue,
427}
428
429#[cfg(feature = "test-support")]
430impl FakeHttpClient {
431    pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
432    where
433        Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
434        F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
435    {
436        Arc::new(HttpClientWithUrl {
437            base_url: Mutex::new("http://test.example".into()),
438            client: HttpClientWithProxy {
439                client: Arc::new(Self {
440                    handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
441                    user_agent: HeaderValue::from_static(type_name::<Self>()),
442                }),
443                proxy: None,
444            },
445        })
446    }
447
448    pub fn with_404_response() -> Arc<HttpClientWithUrl> {
449        log::warn!("Using fake HTTP client with 404 response");
450        Self::create(|_| async move {
451            Ok(Response::builder()
452                .status(404)
453                .body(Default::default())
454                .unwrap())
455        })
456    }
457
458    pub fn with_200_response() -> Arc<HttpClientWithUrl> {
459        log::warn!("Using fake HTTP client with 200 response");
460        Self::create(|_| async move {
461            Ok(Response::builder()
462                .status(200)
463                .body(Default::default())
464                .unwrap())
465        })
466    }
467
468    pub fn replace_handler<Fut, F>(&self, new_handler: F)
469    where
470        Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
471        F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
472    {
473        let mut handler = self.handler.lock();
474        let old_handler = handler.take().unwrap();
475        *handler = Some(Arc::new(move |req| {
476            Box::pin(new_handler(old_handler.clone(), req))
477        }));
478    }
479}
480
481#[cfg(feature = "test-support")]
482impl fmt::Debug for FakeHttpClient {
483    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484        f.debug_struct("FakeHttpClient").finish()
485    }
486}
487
488#[cfg(feature = "test-support")]
489impl HttpClient for FakeHttpClient {
490    fn send(
491        &self,
492        req: Request<AsyncBody>,
493    ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
494        ((self.handler.lock().as_ref().unwrap())(req)) as _
495    }
496
497    fn user_agent(&self) -> Option<&HeaderValue> {
498        Some(&self.user_agent)
499    }
500
501    fn proxy(&self) -> Option<&Url> {
502        None
503    }
504
505    fn as_fake(&self) -> &FakeHttpClient {
506        self
507    }
508}