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