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