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