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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct RequestTimeout(pub Duration);
37
38impl RequestTimeout {
39 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 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 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 fn follow_redirects(self, follow: RedirectPolicy) -> Self;
67
68 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#[derive(Deref)]
153pub struct HttpClientWithProxy {
154 #[deref]
155 client: Arc<dyn HttpClient>,
156 proxy: Option<Url>,
157}
158
159impl HttpClientWithProxy {
160 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
210pub 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 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 pub fn base_url(&self) -> String {
254 self.base_url.lock().clone()
255 }
256
257 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 pub fn build_url(&self, path: &str) -> String {
265 format!("{}{}", self.base_url(), path)
266 }
267
268 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 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 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}