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