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