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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct RequestTimeout(pub Duration);
51
52impl RequestTimeout {
53 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 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 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 fn follow_redirects(self, follow: RedirectPolicy) -> Self;
81
82 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#[derive(Deref)]
167pub struct HttpClientWithProxy {
168 #[deref]
169 client: Arc<dyn HttpClient>,
170 proxy: Option<Url>,
171}
172
173impl HttpClientWithProxy {
174 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
224pub 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 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 pub fn base_url(&self) -> String {
268 self.base_url.lock().clone()
269 }
270
271 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 pub fn build_url(&self, path: &str) -> String {
279 format!("{}{}", self.base_url(), path)
280 }
281
282 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 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 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}