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