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