Skip to main content

fc_gpui_http_client/
http_client.rs

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