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