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 LLM URL using the given path.
240 pub fn build_zed_llm_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://llm-staging.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
256impl HttpClient for Arc<HttpClientWithUrl> {
257 fn send(
258 &self,
259 req: Request<AsyncBody>,
260 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
261 self.client.send(req)
262 }
263
264 fn user_agent(&self) -> Option<&HeaderValue> {
265 self.client.user_agent()
266 }
267
268 fn proxy(&self) -> Option<&Url> {
269 self.client.proxy.as_ref()
270 }
271
272 fn type_name(&self) -> &'static str {
273 self.client.type_name()
274 }
275}
276
277impl HttpClient for HttpClientWithUrl {
278 fn send(
279 &self,
280 req: Request<AsyncBody>,
281 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
282 self.client.send(req)
283 }
284
285 fn user_agent(&self) -> Option<&HeaderValue> {
286 self.client.user_agent()
287 }
288
289 fn proxy(&self) -> Option<&Url> {
290 self.client.proxy.as_ref()
291 }
292
293 fn type_name(&self) -> &'static str {
294 self.client.type_name()
295 }
296}
297
298pub fn read_proxy_from_env() -> Option<Url> {
299 const ENV_VARS: &[&str] = &[
300 "ALL_PROXY",
301 "all_proxy",
302 "HTTPS_PROXY",
303 "https_proxy",
304 "HTTP_PROXY",
305 "http_proxy",
306 ];
307
308 ENV_VARS
309 .iter()
310 .find_map(|var| std::env::var(var).ok())
311 .and_then(|env| env.parse().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 fn type_name(&self) -> &'static str {
345 type_name::<Self>()
346 }
347}
348
349#[cfg(feature = "test-support")]
350type FakeHttpHandler = Box<
351 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
352 + Send
353 + Sync
354 + 'static,
355>;
356
357#[cfg(feature = "test-support")]
358pub struct FakeHttpClient {
359 handler: FakeHttpHandler,
360 user_agent: HeaderValue,
361}
362
363#[cfg(feature = "test-support")]
364impl FakeHttpClient {
365 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
366 where
367 Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
368 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
369 {
370 Arc::new(HttpClientWithUrl {
371 base_url: Mutex::new("http://test.example".into()),
372 client: HttpClientWithProxy {
373 client: Arc::new(Self {
374 handler: Box::new(move |req| Box::pin(handler(req))),
375 user_agent: HeaderValue::from_static(type_name::<Self>()),
376 }),
377 proxy: None,
378 },
379 })
380 }
381
382 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
383 Self::create(|_| async move {
384 Ok(Response::builder()
385 .status(404)
386 .body(Default::default())
387 .unwrap())
388 })
389 }
390
391 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
392 Self::create(|_| async move {
393 Ok(Response::builder()
394 .status(200)
395 .body(Default::default())
396 .unwrap())
397 })
398 }
399}
400
401#[cfg(feature = "test-support")]
402impl fmt::Debug for FakeHttpClient {
403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404 f.debug_struct("FakeHttpClient").finish()
405 }
406}
407
408#[cfg(feature = "test-support")]
409impl HttpClient for FakeHttpClient {
410 fn send(
411 &self,
412 req: Request<AsyncBody>,
413 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
414 let future = (self.handler)(req);
415 future
416 }
417
418 fn user_agent(&self) -> Option<&HeaderValue> {
419 Some(&self.user_agent)
420 }
421
422 fn proxy(&self) -> Option<&Url> {
423 None
424 }
425
426 fn type_name(&self) -> &'static str {
427 type_name::<Self>()
428 }
429}