Skip to main content

http_client/
http_client.rs

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