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