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