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