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