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