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