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