1mod async_body;
2
3pub use anyhow::{Result, anyhow};
4pub use async_body::{AsyncBody, Inner, Json};
5pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
6use http::{HeaderName, HeaderValue};
7
8use futures::future::BoxFuture;
9use parking_lot::Mutex;
10use serde::Serialize;
11#[cfg(feature = "test-support")]
12use std::{any::type_name, fmt};
13use std::{ops::Deref, sync::Arc, time::Duration};
14pub use url::{Host, Url};
15
16#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
17pub enum RedirectPolicy {
18 #[default]
19 NoFollow,
20 FollowLimit(u32),
21 FollowAll,
22}
23pub struct FollowRedirects(pub bool);
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct RequestTimeout(pub Duration);
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 fn timeout(self, timeout: Duration) -> Self;
53}
54
55impl HttpRequestExt for http::request::Builder {
56 fn follow_redirects(self, follow: RedirectPolicy) -> Self {
57 self.extension(follow)
58 }
59
60 fn timeout(self, timeout: Duration) -> Self {
61 debug_assert!(!timeout.is_zero(), "timeout must be positive");
62 self.extension(RequestTimeout(timeout))
63 }
64}
65
66#[derive(Default, Clone, Debug)]
72pub struct CustomHeaders(Arc<[(HeaderName, HeaderValue)]>);
73
74impl CustomHeaders {
75 pub fn new(headers: Vec<(HeaderName, HeaderValue)>) -> Self {
76 Self(headers.into())
77 }
78
79 pub fn is_empty(&self) -> bool {
80 self.0.is_empty()
81 }
82
83 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&HeaderName, &HeaderValue)> {
84 self.0.iter().map(|(n, v)| (n, v))
85 }
86}
87
88impl PartialEq for CustomHeaders {
89 fn eq(&self, other: &Self) -> bool {
90 self.0.len() == other.0.len()
91 && self
92 .0
93 .iter()
94 .zip(other.0.iter())
95 .all(|(a, b)| a.0 == b.0 && a.1 == b.1)
96 }
97}
98
99pub trait RequestBuilderExt {
100 fn extra_headers(self, headers: &CustomHeaders) -> Self;
102}
103
104impl RequestBuilderExt for http::request::Builder {
105 fn extra_headers(mut self, headers: &CustomHeaders) -> Self {
106 if headers.is_empty() {
107 return self;
108 }
109 if let Some(map) = self.headers_mut() {
110 for (name, value) in headers.iter() {
111 map.append(name.clone(), value.clone());
112 }
113 }
114 self
115 }
116}
117
118pub trait HttpClient: 'static + Send + Sync {
119 fn user_agent(&self) -> Option<&HeaderValue>;
120
121 fn proxy(&self) -> Option<&Url>;
122
123 fn send(
124 &self,
125 req: http::Request<AsyncBody>,
126 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
127
128 fn get(
129 &self,
130 uri: &str,
131 body: AsyncBody,
132 follow_redirects: bool,
133 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
134 let request = Builder::new()
135 .uri(uri)
136 .follow_redirects(if follow_redirects {
137 RedirectPolicy::FollowAll
138 } else {
139 RedirectPolicy::NoFollow
140 })
141 .body(body);
142
143 match request {
144 Ok(request) => self.send(request),
145 Err(e) => Box::pin(async move { Err(e.into()) }),
146 }
147 }
148
149 fn post_json(
150 &self,
151 uri: &str,
152 body: AsyncBody,
153 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
154 let request = Builder::new()
155 .uri(uri)
156 .method(Method::POST)
157 .header("Content-Type", "application/json")
158 .body(body);
159
160 match request {
161 Ok(request) => self.send(request),
162 Err(e) => Box::pin(async move { Err(e.into()) }),
163 }
164 }
165
166 #[cfg(feature = "test-support")]
167 fn as_fake(&self) -> &FakeHttpClient {
168 panic!("called as_fake on {}", type_name::<Self>())
169 }
170}
171
172pub struct HttpClientWithProxy {
174 client: Arc<dyn HttpClient>,
175 proxy: Option<Url>,
176}
177
178impl HttpClientWithProxy {
179 pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
181 let proxy_url = proxy_url
182 .and_then(|proxy| proxy.parse().ok())
183 .or_else(read_proxy_from_env);
184
185 Self::new_url(client, proxy_url)
186 }
187 pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
188 Self {
189 client,
190 proxy: proxy_url,
191 }
192 }
193}
194
195impl Deref for HttpClientWithProxy {
196 type Target = Arc<dyn HttpClient>;
197
198 #[inline]
199 fn deref(&self) -> &Self::Target {
200 &self.client
201 }
202}
203
204impl HttpClient for HttpClientWithProxy {
205 fn send(
206 &self,
207 req: Request<AsyncBody>,
208 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
209 self.client.send(req)
210 }
211
212 fn user_agent(&self) -> Option<&HeaderValue> {
213 self.client.user_agent()
214 }
215
216 fn proxy(&self) -> Option<&Url> {
217 self.proxy.as_ref()
218 }
219
220 #[cfg(feature = "test-support")]
221 fn as_fake(&self) -> &FakeHttpClient {
222 self.client.as_fake()
223 }
224}
225
226pub struct HttpClientWithUrl {
228 base_url: Mutex<String>,
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 Deref for HttpClientWithUrl {
336 type Target = HttpClientWithProxy;
337
338 #[inline]
339 fn deref(&self) -> &Self::Target {
340 &self.client
341 }
342}
343
344impl HttpClient for HttpClientWithUrl {
345 fn send(
346 &self,
347 req: Request<AsyncBody>,
348 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
349 self.client.send(req)
350 }
351
352 fn user_agent(&self) -> Option<&HeaderValue> {
353 self.client.user_agent()
354 }
355
356 fn proxy(&self) -> Option<&Url> {
357 self.client.proxy.as_ref()
358 }
359
360 #[cfg(feature = "test-support")]
361 fn as_fake(&self) -> &FakeHttpClient {
362 self.client.as_fake()
363 }
364}
365
366pub fn read_proxy_from_env() -> Option<Url> {
367 const ENV_VARS: &[&str] = &[
368 "ALL_PROXY",
369 "all_proxy",
370 "HTTPS_PROXY",
371 "https_proxy",
372 "HTTP_PROXY",
373 "http_proxy",
374 ];
375
376 ENV_VARS
377 .iter()
378 .find_map(|var| std::env::var(var).ok())
379 .and_then(|env| env.parse().ok())
380}
381
382pub fn read_no_proxy_from_env() -> Option<String> {
383 const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
384
385 ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
386}
387
388pub struct BlockedHttpClient;
389
390impl BlockedHttpClient {
391 pub fn new() -> Self {
392 BlockedHttpClient
393 }
394}
395
396impl HttpClient for BlockedHttpClient {
397 fn send(
398 &self,
399 _req: Request<AsyncBody>,
400 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
401 Box::pin(async {
402 Err(std::io::Error::new(
403 std::io::ErrorKind::PermissionDenied,
404 "BlockedHttpClient disallowed request",
405 )
406 .into())
407 })
408 }
409
410 fn user_agent(&self) -> Option<&HeaderValue> {
411 None
412 }
413
414 fn proxy(&self) -> Option<&Url> {
415 None
416 }
417
418 #[cfg(feature = "test-support")]
419 fn as_fake(&self) -> &FakeHttpClient {
420 panic!("called as_fake on {}", type_name::<Self>())
421 }
422}
423
424#[cfg(feature = "test-support")]
425type FakeHttpHandler = Arc<
426 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
427 + Send
428 + Sync
429 + 'static,
430>;
431
432#[cfg(feature = "test-support")]
433pub struct FakeHttpClient {
434 handler: Mutex<Option<FakeHttpHandler>>,
435 user_agent: HeaderValue,
436}
437
438#[cfg(feature = "test-support")]
439impl FakeHttpClient {
440 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
441 where
442 Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
443 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
444 {
445 Arc::new(HttpClientWithUrl {
446 base_url: Mutex::new("http://test.example".into()),
447 client: HttpClientWithProxy {
448 client: Arc::new(Self {
449 handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
450 user_agent: HeaderValue::from_static(type_name::<Self>()),
451 }),
452 proxy: None,
453 },
454 })
455 }
456
457 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
458 log::warn!("Using fake HTTP client with 404 response");
459 Self::create(|_| async move {
460 Ok(Response::builder()
461 .status(404)
462 .body(Default::default())
463 .unwrap())
464 })
465 }
466
467 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
468 log::warn!("Using fake HTTP client with 200 response");
469 Self::create(|_| async move {
470 Ok(Response::builder()
471 .status(200)
472 .body(Default::default())
473 .unwrap())
474 })
475 }
476
477 pub fn replace_handler<Fut, F>(&self, new_handler: F)
478 where
479 Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
480 F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
481 {
482 let mut handler = self.handler.lock();
483 let old_handler = handler.take().unwrap();
484 *handler = Some(Arc::new(move |req| {
485 Box::pin(new_handler(old_handler.clone(), req))
486 }));
487 }
488}
489
490#[cfg(feature = "test-support")]
491impl fmt::Debug for FakeHttpClient {
492 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493 f.debug_struct("FakeHttpClient").finish()
494 }
495}
496
497#[cfg(feature = "test-support")]
498impl HttpClient for FakeHttpClient {
499 fn send(
500 &self,
501 req: Request<AsyncBody>,
502 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
503 ((self.handler.lock().as_ref().unwrap())(req)) as _
504 }
505
506 fn user_agent(&self) -> Option<&HeaderValue> {
507 Some(&self.user_agent)
508 }
509
510 fn proxy(&self) -> Option<&Url> {
511 None
512 }
513
514 fn as_fake(&self) -> &FakeHttpClient {
515 self
516 }
517}