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