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(
230 &self,
231 path: &str,
232 query: &[(&str, &str)],
233 use_cloud: bool,
234 ) -> Result<Url> {
235 let base_url = self.base_url();
236 let base_api_url = match base_url.as_ref() {
237 "https://zed.dev" => {
238 if use_cloud {
239 "https://cloud.zed.dev"
240 } else {
241 "https://llm.zed.dev"
242 }
243 }
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 proxy(&self) -> Option<&Url> {
265 self.client.proxy.as_ref()
266 }
267
268 fn type_name(&self) -> &'static str {
269 self.client.type_name()
270 }
271}
272
273impl HttpClient for HttpClientWithUrl {
274 fn send(
275 &self,
276 req: Request<AsyncBody>,
277 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
278 self.client.send(req)
279 }
280
281 fn proxy(&self) -> Option<&Url> {
282 self.client.proxy.as_ref()
283 }
284
285 fn type_name(&self) -> &'static str {
286 self.client.type_name()
287 }
288}
289
290pub fn read_proxy_from_env() -> Option<Url> {
291 const ENV_VARS: &[&str] = &[
292 "ALL_PROXY",
293 "all_proxy",
294 "HTTPS_PROXY",
295 "https_proxy",
296 "HTTP_PROXY",
297 "http_proxy",
298 ];
299
300 ENV_VARS
301 .iter()
302 .find_map(|var| std::env::var(var).ok())
303 .and_then(|env| env.parse().ok())
304}
305
306pub struct BlockedHttpClient;
307
308impl BlockedHttpClient {
309 pub fn new() -> Self {
310 BlockedHttpClient
311 }
312}
313
314impl HttpClient for BlockedHttpClient {
315 fn send(
316 &self,
317 _req: Request<AsyncBody>,
318 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
319 Box::pin(async {
320 Err(std::io::Error::new(
321 std::io::ErrorKind::PermissionDenied,
322 "BlockedHttpClient disallowed request",
323 )
324 .into())
325 })
326 }
327
328 fn proxy(&self) -> Option<&Url> {
329 None
330 }
331
332 fn type_name(&self) -> &'static str {
333 type_name::<Self>()
334 }
335}
336
337#[cfg(feature = "test-support")]
338type FakeHttpHandler = Box<
339 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
340 + Send
341 + Sync
342 + 'static,
343>;
344
345#[cfg(feature = "test-support")]
346pub struct FakeHttpClient {
347 handler: FakeHttpHandler,
348}
349
350#[cfg(feature = "test-support")]
351impl FakeHttpClient {
352 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
353 where
354 Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
355 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
356 {
357 Arc::new(HttpClientWithUrl {
358 base_url: Mutex::new("http://test.example".into()),
359 client: HttpClientWithProxy {
360 client: Arc::new(Self {
361 handler: Box::new(move |req| Box::pin(handler(req))),
362 }),
363 proxy: None,
364 },
365 })
366 }
367
368 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
369 Self::create(|_| async move {
370 Ok(Response::builder()
371 .status(404)
372 .body(Default::default())
373 .unwrap())
374 })
375 }
376
377 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
378 Self::create(|_| async move {
379 Ok(Response::builder()
380 .status(200)
381 .body(Default::default())
382 .unwrap())
383 })
384 }
385}
386
387#[cfg(feature = "test-support")]
388impl fmt::Debug for FakeHttpClient {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 f.debug_struct("FakeHttpClient").finish()
391 }
392}
393
394#[cfg(feature = "test-support")]
395impl HttpClient for FakeHttpClient {
396 fn send(
397 &self,
398 req: Request<AsyncBody>,
399 ) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
400 let future = (self.handler)(req);
401 future
402 }
403
404 fn proxy(&self) -> Option<&Url> {
405 None
406 }
407
408 fn type_name(&self) -> &'static str {
409 type_name::<Self>()
410 }
411}