1pub mod github;
2
3pub use anyhow::{anyhow, Result};
4use derive_more::Deref;
5use futures::future::BoxFuture;
6use futures_lite::FutureExt;
7use isahc::config::{Configurable, RedirectPolicy};
8pub use isahc::http;
9pub use isahc::{
10 http::{Method, StatusCode, Uri},
11 AsyncBody, Error, HttpClient as IsahcHttpClient, Request, Response,
12};
13#[cfg(feature = "test-support")]
14use std::fmt;
15use std::{
16 sync::{Arc, Mutex},
17 time::Duration,
18};
19pub use url::Url;
20
21pub trait HttpClient: Send + Sync {
22 fn send(
23 &self,
24 req: Request<AsyncBody>,
25 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
26
27 fn get<'a>(
28 &'a self,
29 uri: &str,
30 body: AsyncBody,
31 follow_redirects: bool,
32 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
33 let request = isahc::Request::builder()
34 .redirect_policy(if follow_redirects {
35 RedirectPolicy::Follow
36 } else {
37 RedirectPolicy::None
38 })
39 .method(Method::GET)
40 .uri(uri)
41 .body(body);
42 match request {
43 Ok(request) => self.send(request),
44 Err(error) => async move { Err(error.into()) }.boxed(),
45 }
46 }
47
48 fn post_json<'a>(
49 &'a self,
50 uri: &str,
51 body: AsyncBody,
52 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
53 let request = isahc::Request::builder()
54 .method(Method::POST)
55 .uri(uri)
56 .header("Content-Type", "application/json")
57 .body(body);
58 match request {
59 Ok(request) => self.send(request),
60 Err(error) => async move { Err(error.into()) }.boxed(),
61 }
62 }
63
64 fn proxy(&self) -> Option<&Uri>;
65}
66
67/// An [`HttpClient`] that may have a proxy.
68#[derive(Deref)]
69pub struct HttpClientWithProxy {
70 #[deref]
71 client: Arc<dyn HttpClient>,
72 proxy: Option<Uri>,
73}
74
75impl HttpClientWithProxy {
76 /// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
77 pub fn new(user_agent: Option<String>, proxy_url: Option<String>) -> Self {
78 let proxy_url = proxy_url
79 .and_then(|input| {
80 input
81 .parse::<Uri>()
82 .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
83 .ok()
84 })
85 .or_else(read_proxy_from_env);
86
87 Self {
88 client: client(user_agent, proxy_url.clone()),
89 proxy: proxy_url,
90 }
91 }
92}
93
94impl HttpClient for HttpClientWithProxy {
95 fn send(
96 &self,
97 req: Request<AsyncBody>,
98 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
99 self.client.send(req)
100 }
101
102 fn proxy(&self) -> Option<&Uri> {
103 self.proxy.as_ref()
104 }
105}
106
107impl HttpClient for Arc<HttpClientWithProxy> {
108 fn send(
109 &self,
110 req: Request<AsyncBody>,
111 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
112 self.client.send(req)
113 }
114
115 fn proxy(&self) -> Option<&Uri> {
116 self.proxy.as_ref()
117 }
118}
119
120/// An [`HttpClient`] that has a base URL.
121pub struct HttpClientWithUrl {
122 base_url: Mutex<String>,
123 client: HttpClientWithProxy,
124}
125
126impl HttpClientWithUrl {
127 /// Returns a new [`HttpClientWithUrl`] with the given base URL.
128 pub fn new(
129 base_url: impl Into<String>,
130 user_agent: Option<String>,
131 proxy_url: Option<String>,
132 ) -> Self {
133 let client = HttpClientWithProxy::new(user_agent, proxy_url);
134
135 Self {
136 base_url: Mutex::new(base_url.into()),
137 client,
138 }
139 }
140
141 /// Returns the base URL.
142 pub fn base_url(&self) -> String {
143 self.base_url
144 .lock()
145 .map_or_else(|_| Default::default(), |url| url.clone())
146 }
147
148 /// Sets the base URL.
149 pub fn set_base_url(&self, base_url: impl Into<String>) {
150 let base_url = base_url.into();
151 self.base_url
152 .lock()
153 .map(|mut url| {
154 *url = base_url;
155 })
156 .ok();
157 }
158
159 /// Builds a URL using the given path.
160 pub fn build_url(&self, path: &str) -> String {
161 format!("{}{}", self.base_url(), path)
162 }
163
164 /// Builds a Zed API URL using the given path.
165 pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
166 let base_url = self.base_url();
167 let base_api_url = match base_url.as_ref() {
168 "https://zed.dev" => "https://api.zed.dev",
169 "https://staging.zed.dev" => "https://api-staging.zed.dev",
170 "http://localhost:3000" => "http://localhost:8080",
171 other => other,
172 };
173
174 Ok(Url::parse_with_params(
175 &format!("{}{}", base_api_url, path),
176 query,
177 )?)
178 }
179
180 /// Builds a Zed LLM URL using the given path.
181 pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
182 let base_url = self.base_url();
183 let base_api_url = match base_url.as_ref() {
184 "https://zed.dev" => "https://llm.zed.dev",
185 "https://staging.zed.dev" => "https://llm-staging.zed.dev",
186 "http://localhost:3000" => "http://localhost:8080",
187 other => other,
188 };
189
190 Ok(Url::parse_with_params(
191 &format!("{}{}", base_api_url, path),
192 query,
193 )?)
194 }
195}
196
197impl HttpClient for Arc<HttpClientWithUrl> {
198 fn send(
199 &self,
200 req: Request<AsyncBody>,
201 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
202 self.client.send(req)
203 }
204
205 fn proxy(&self) -> Option<&Uri> {
206 self.client.proxy.as_ref()
207 }
208}
209
210impl HttpClient for HttpClientWithUrl {
211 fn send(
212 &self,
213 req: Request<AsyncBody>,
214 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
215 self.client.send(req)
216 }
217
218 fn proxy(&self) -> Option<&Uri> {
219 self.client.proxy.as_ref()
220 }
221}
222
223pub fn client(user_agent: Option<String>, proxy: Option<Uri>) -> Arc<dyn HttpClient> {
224 let mut builder = isahc::HttpClient::builder()
225 // Some requests to Qwen2 models on Runpod can take 32+ seconds,
226 // especially if there's a cold boot involved. We may need to have
227 // those requests use a different http client, because global timeouts
228 // of 50 and 60 seconds, respectively, would be very high!
229 .connect_timeout(Duration::from_secs(5))
230 .low_speed_timeout(100, Duration::from_secs(30))
231 .proxy(proxy.clone());
232 if let Some(user_agent) = user_agent {
233 builder = builder.default_header("User-Agent", user_agent);
234 }
235
236 Arc::new(HttpClientWithProxy {
237 client: Arc::new(builder.build().unwrap()),
238 proxy,
239 })
240}
241
242fn read_proxy_from_env() -> Option<Uri> {
243 const ENV_VARS: &[&str] = &[
244 "ALL_PROXY",
245 "all_proxy",
246 "HTTPS_PROXY",
247 "https_proxy",
248 "HTTP_PROXY",
249 "http_proxy",
250 ];
251
252 for var in ENV_VARS {
253 if let Ok(env) = std::env::var(var) {
254 return env.parse::<Uri>().ok();
255 }
256 }
257
258 None
259}
260
261impl HttpClient for isahc::HttpClient {
262 fn send(
263 &self,
264 req: Request<AsyncBody>,
265 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
266 let client = self.clone();
267 Box::pin(async move { client.send_async(req).await })
268 }
269
270 fn proxy(&self) -> Option<&Uri> {
271 None
272 }
273}
274
275#[cfg(feature = "test-support")]
276type FakeHttpHandler = Box<
277 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
278 + Send
279 + Sync
280 + 'static,
281>;
282
283#[cfg(feature = "test-support")]
284pub struct FakeHttpClient {
285 handler: FakeHttpHandler,
286}
287
288#[cfg(feature = "test-support")]
289impl FakeHttpClient {
290 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
291 where
292 Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
293 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
294 {
295 Arc::new(HttpClientWithUrl {
296 base_url: Mutex::new("http://test.example".into()),
297 client: HttpClientWithProxy {
298 client: Arc::new(Self {
299 handler: Box::new(move |req| Box::pin(handler(req))),
300 }),
301 proxy: None,
302 },
303 })
304 }
305
306 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
307 Self::create(|_| async move {
308 Ok(Response::builder()
309 .status(404)
310 .body(Default::default())
311 .unwrap())
312 })
313 }
314
315 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
316 Self::create(|_| async move {
317 Ok(Response::builder()
318 .status(200)
319 .body(Default::default())
320 .unwrap())
321 })
322 }
323}
324
325#[cfg(feature = "test-support")]
326impl fmt::Debug for FakeHttpClient {
327 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328 f.debug_struct("FakeHttpClient").finish()
329 }
330}
331
332#[cfg(feature = "test-support")]
333impl HttpClient for FakeHttpClient {
334 fn send(
335 &self,
336 req: Request<AsyncBody>,
337 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
338 let future = (self.handler)(req);
339 Box::pin(async move { future.await.map(Into::into) })
340 }
341
342 fn proxy(&self) -> Option<&Uri> {
343 None
344 }
345}