1mod async_body;
2pub mod github;
3pub mod github_download;
4
5pub use anyhow::{Result, anyhow};
6pub use async_body::{AsyncBody, Inner};
7use derive_more::Deref;
8use http::HeaderValue;
9pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
10
11use futures::{
12 FutureExt as _,
13 future::{self, BoxFuture},
14};
15use parking_lot::Mutex;
16#[cfg(feature = "test-support")]
17use std::fmt;
18use std::{any::type_name, sync::Arc};
19pub use url::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 /// Conditionally modify self with the given closure.
32 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 /// Conditionally unwrap and modify self with the given closure, if the given option is Some.
40 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 /// Whether or not to follow redirects
51 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
60pub trait HttpClient: 'static + Send + Sync {
61 fn type_name(&self) -> &'static str;
62
63 fn user_agent(&self) -> Option<&HeaderValue>;
64
65 fn send(
66 &self,
67 req: http::Request<AsyncBody>,
68 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
69
70 fn get(
71 &self,
72 uri: &str,
73 body: AsyncBody,
74 follow_redirects: bool,
75 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
76 let request = Builder::new()
77 .uri(uri)
78 .follow_redirects(if follow_redirects {
79 RedirectPolicy::FollowAll
80 } else {
81 RedirectPolicy::NoFollow
82 })
83 .body(body);
84
85 match request {
86 Ok(request) => self.send(request),
87 Err(e) => Box::pin(async move { Err(e.into()) }),
88 }
89 }
90
91 fn post_json(
92 &self,
93 uri: &str,
94 body: AsyncBody,
95 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
96 let request = Builder::new()
97 .uri(uri)
98 .method(Method::POST)
99 .header("Content-Type", "application/json")
100 .body(body);
101
102 match request {
103 Ok(request) => self.send(request),
104 Err(e) => Box::pin(async move { Err(e.into()) }),
105 }
106 }
107
108 fn proxy(&self) -> Option<&Url>;
109
110 #[cfg(feature = "test-support")]
111 fn as_fake(&self) -> &FakeHttpClient {
112 panic!("called as_fake on {}", type_name::<Self>())
113 }
114
115 fn send_multipart_form<'a>(
116 &'a self,
117 _url: &str,
118 _request: reqwest::multipart::Form,
119 ) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
120 future::ready(Err(anyhow!("not implemented"))).boxed()
121 }
122}
123
124/// An [`HttpClient`] that may have a proxy.
125#[derive(Deref)]
126pub struct HttpClientWithProxy {
127 #[deref]
128 client: Arc<dyn HttpClient>,
129 proxy: Option<Url>,
130}
131
132impl HttpClientWithProxy {
133 /// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
134 pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
135 let proxy_url = proxy_url
136 .and_then(|proxy| proxy.parse().ok())
137 .or_else(read_proxy_from_env);
138
139 Self::new_url(client, proxy_url)
140 }
141 pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
142 Self {
143 client,
144 proxy: proxy_url,
145 }
146 }
147}
148
149impl HttpClient for HttpClientWithProxy {
150 fn send(
151 &self,
152 req: Request<AsyncBody>,
153 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
154 self.client.send(req)
155 }
156
157 fn user_agent(&self) -> Option<&HeaderValue> {
158 self.client.user_agent()
159 }
160
161 fn proxy(&self) -> Option<&Url> {
162 self.proxy.as_ref()
163 }
164
165 fn type_name(&self) -> &'static str {
166 self.client.type_name()
167 }
168
169 #[cfg(feature = "test-support")]
170 fn as_fake(&self) -> &FakeHttpClient {
171 self.client.as_fake()
172 }
173
174 fn send_multipart_form<'a>(
175 &'a self,
176 url: &str,
177 form: reqwest::multipart::Form,
178 ) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
179 self.client.send_multipart_form(url, form)
180 }
181}
182
183/// An [`HttpClient`] that has a base URL.
184pub struct HttpClientWithUrl {
185 base_url: Mutex<String>,
186 client: HttpClientWithProxy,
187}
188
189impl std::ops::Deref for HttpClientWithUrl {
190 type Target = HttpClientWithProxy;
191
192 fn deref(&self) -> &Self::Target {
193 &self.client
194 }
195}
196
197impl HttpClientWithUrl {
198 /// Returns a new [`HttpClientWithUrl`] with the given base URL.
199 pub fn new(
200 client: Arc<dyn HttpClient>,
201 base_url: impl Into<String>,
202 proxy_url: Option<String>,
203 ) -> Self {
204 let client = HttpClientWithProxy::new(client, proxy_url);
205
206 Self {
207 base_url: Mutex::new(base_url.into()),
208 client,
209 }
210 }
211
212 pub fn new_url(
213 client: Arc<dyn HttpClient>,
214 base_url: impl Into<String>,
215 proxy_url: Option<Url>,
216 ) -> Self {
217 let client = HttpClientWithProxy::new_url(client, proxy_url);
218
219 Self {
220 base_url: Mutex::new(base_url.into()),
221 client,
222 }
223 }
224
225 /// Returns the base URL.
226 pub fn base_url(&self) -> String {
227 self.base_url.lock().clone()
228 }
229
230 /// Sets the base URL.
231 pub fn set_base_url(&self, base_url: impl Into<String>) {
232 let base_url = base_url.into();
233 *self.base_url.lock() = base_url;
234 }
235
236 /// Builds a URL using the given path.
237 pub fn build_url(&self, path: &str) -> String {
238 format!("{}{}", self.base_url(), path)
239 }
240
241 /// Builds a Zed API URL using the given path.
242 pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
243 let base_url = self.base_url();
244 let base_api_url = match base_url.as_ref() {
245 "https://zed.dev" => "https://api.zed.dev",
246 "https://staging.zed.dev" => "https://api-staging.zed.dev",
247 "http://localhost:3000" => "http://localhost:8080",
248 other => other,
249 };
250
251 Ok(Url::parse_with_params(
252 &format!("{}{}", base_api_url, path),
253 query,
254 )?)
255 }
256
257 /// Builds a Zed Cloud URL using the given path.
258 pub fn build_zed_cloud_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
259 let base_url = self.base_url();
260 let base_api_url = match base_url.as_ref() {
261 "https://zed.dev" => "https://cloud.zed.dev",
262 "https://staging.zed.dev" => "https://cloud.zed.dev",
263 "http://localhost:3000" => "http://localhost:8787",
264 other => other,
265 };
266
267 Ok(Url::parse_with_params(
268 &format!("{}{}", base_api_url, path),
269 query,
270 )?)
271 }
272
273 /// Builds a Zed LLM URL using the given path.
274 pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
275 let base_url = self.base_url();
276 let base_api_url = match base_url.as_ref() {
277 "https://zed.dev" => "https://cloud.zed.dev",
278 "https://staging.zed.dev" => "https://llm-staging.zed.dev",
279 "http://localhost:3000" => "http://localhost:8787",
280 other => other,
281 };
282
283 Ok(Url::parse_with_params(
284 &format!("{}{}", base_api_url, path),
285 query,
286 )?)
287 }
288}
289
290impl HttpClient for HttpClientWithUrl {
291 fn send(
292 &self,
293 req: Request<AsyncBody>,
294 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
295 self.client.send(req)
296 }
297
298 fn user_agent(&self) -> Option<&HeaderValue> {
299 self.client.user_agent()
300 }
301
302 fn proxy(&self) -> Option<&Url> {
303 self.client.proxy.as_ref()
304 }
305
306 fn type_name(&self) -> &'static str {
307 self.client.type_name()
308 }
309
310 #[cfg(feature = "test-support")]
311 fn as_fake(&self) -> &FakeHttpClient {
312 self.client.as_fake()
313 }
314
315 fn send_multipart_form<'a>(
316 &'a self,
317 url: &str,
318 request: reqwest::multipart::Form,
319 ) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
320 self.client.send_multipart_form(url, request)
321 }
322}
323
324pub fn read_proxy_from_env() -> Option<Url> {
325 const ENV_VARS: &[&str] = &[
326 "ALL_PROXY",
327 "all_proxy",
328 "HTTPS_PROXY",
329 "https_proxy",
330 "HTTP_PROXY",
331 "http_proxy",
332 ];
333
334 ENV_VARS
335 .iter()
336 .find_map(|var| std::env::var(var).ok())
337 .and_then(|env| env.parse().ok())
338}
339
340pub fn read_no_proxy_from_env() -> Option<String> {
341 const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
342
343 ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
344}
345
346pub struct BlockedHttpClient;
347
348impl BlockedHttpClient {
349 pub const 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 ((self.handler.lock().as_ref().unwrap())(req)) as _
464 }
465
466 fn user_agent(&self) -> Option<&HeaderValue> {
467 Some(&self.user_agent)
468 }
469
470 fn proxy(&self) -> Option<&Url> {
471 None
472 }
473
474 fn type_name(&self) -> &'static str {
475 type_name::<Self>()
476 }
477
478 fn as_fake(&self) -> &FakeHttpClient {
479 self
480 }
481}