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, Mutex},
15 time::Duration,
16};
17pub use url::Url;
18
19pub struct ReadTimeout(pub Duration);
20#[derive(Default, Debug, Clone)]
21pub enum RedirectPolicy {
22 #[default]
23 NoFollow,
24 FollowLimit(u32),
25 FollowAll,
26}
27pub struct FollowRedirects(pub bool);
28
29pub trait HttpRequestExt {
30 /// Set a read timeout on the request.
31 /// For isahc, this is the low_speed_timeout.
32 /// For other clients, this is the timeout used for read calls when reading the response.
33 /// In all cases this prevents servers stalling completely, but allows them to send data slowly.
34 fn read_timeout(self, timeout: Duration) -> Self;
35 /// Whether or not to follow redirects
36 fn follow_redirects(self, follow: RedirectPolicy) -> Self;
37}
38
39impl HttpRequestExt for http::request::Builder {
40 fn read_timeout(self, timeout: Duration) -> Self {
41 self.extension(ReadTimeout(timeout))
42 }
43
44 fn follow_redirects(self, follow: RedirectPolicy) -> Self {
45 self.extension(follow)
46 }
47}
48
49pub trait HttpClient: 'static + Send + Sync {
50 fn send(
51 &self,
52 req: http::Request<AsyncBody>,
53 ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>>;
54
55 fn get<'a>(
56 &'a self,
57 uri: &str,
58 body: AsyncBody,
59 follow_redirects: bool,
60 ) -> BoxFuture<'a, Result<Response<AsyncBody>, anyhow::Error>> {
61 let request = Builder::new()
62 .uri(uri)
63 .follow_redirects(if follow_redirects {
64 RedirectPolicy::FollowAll
65 } else {
66 RedirectPolicy::NoFollow
67 })
68 .body(body);
69
70 match request {
71 Ok(request) => Box::pin(async move { self.send(request).await.map_err(Into::into) }),
72 Err(e) => Box::pin(async move { Err(e.into()) }),
73 }
74 }
75
76 fn post_json<'a>(
77 &'a self,
78 uri: &str,
79 body: AsyncBody,
80 ) -> BoxFuture<'a, Result<Response<AsyncBody>, anyhow::Error>> {
81 let request = Builder::new()
82 .uri(uri)
83 .method(Method::POST)
84 .header("Content-Type", "application/json")
85 .body(body);
86
87 match request {
88 Ok(request) => Box::pin(async move { self.send(request).await.map_err(Into::into) }),
89 Err(e) => Box::pin(async move { Err(e.into()) }),
90 }
91 }
92
93 fn proxy(&self) -> Option<&Uri>;
94}
95
96/// An [`HttpClient`] that may have a proxy.
97#[derive(Deref)]
98pub struct HttpClientWithProxy {
99 #[deref]
100 client: Arc<dyn HttpClient>,
101 proxy: Option<Uri>,
102}
103
104impl HttpClientWithProxy {
105 /// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
106 pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
107 let proxy_uri = proxy_url
108 .and_then(|proxy| proxy.parse().ok())
109 .or_else(read_proxy_from_env);
110
111 Self::new_uri(client, proxy_uri)
112 }
113 pub fn new_uri(client: Arc<dyn HttpClient>, proxy_uri: Option<Uri>) -> Self {
114 Self {
115 client,
116 proxy: proxy_uri,
117 }
118 }
119}
120
121impl HttpClient for HttpClientWithProxy {
122 fn send(
123 &self,
124 req: Request<AsyncBody>,
125 ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
126 self.client.send(req)
127 }
128
129 fn proxy(&self) -> Option<&Uri> {
130 self.proxy.as_ref()
131 }
132}
133
134impl HttpClient for Arc<HttpClientWithProxy> {
135 fn send(
136 &self,
137 req: Request<AsyncBody>,
138 ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
139 self.client.send(req)
140 }
141
142 fn proxy(&self) -> Option<&Uri> {
143 self.proxy.as_ref()
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_uri(
177 client: Arc<dyn HttpClient>,
178 base_url: impl Into<String>,
179 proxy_uri: Option<Uri>,
180 ) -> Self {
181 let client = HttpClientWithProxy::new_uri(client, proxy_uri);
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://llm.zed.dev",
233 "https://staging.zed.dev" => "https://llm-staging.zed.dev",
234 "http://localhost:3000" => "http://localhost:8080",
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, Result<Response<AsyncBody>, anyhow::Error>> {
250 self.client.send(req)
251 }
252
253 fn proxy(&self) -> Option<&Uri> {
254 self.client.proxy.as_ref()
255 }
256}
257
258impl HttpClient for HttpClientWithUrl {
259 fn send(
260 &self,
261 req: Request<AsyncBody>,
262 ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
263 self.client.send(req)
264 }
265
266 fn proxy(&self) -> Option<&Uri> {
267 self.client.proxy.as_ref()
268 }
269}
270
271pub fn read_proxy_from_env() -> Option<Uri> {
272 const ENV_VARS: &[&str] = &[
273 "ALL_PROXY",
274 "all_proxy",
275 "HTTPS_PROXY",
276 "https_proxy",
277 "HTTP_PROXY",
278 "http_proxy",
279 ];
280
281 for var in ENV_VARS {
282 if let Ok(env) = std::env::var(var) {
283 return env.parse::<Uri>().ok();
284 }
285 }
286
287 None
288}
289
290pub struct BlockedHttpClient;
291
292impl HttpClient for BlockedHttpClient {
293 fn send(
294 &self,
295 _req: Request<AsyncBody>,
296 ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
297 Box::pin(async {
298 Err(std::io::Error::new(
299 std::io::ErrorKind::PermissionDenied,
300 "BlockedHttpClient disallowed request",
301 )
302 .into())
303 })
304 }
305
306 fn proxy(&self) -> Option<&Uri> {
307 None
308 }
309}
310
311#[cfg(feature = "test-support")]
312type FakeHttpHandler = Box<
313 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>>
314 + Send
315 + Sync
316 + 'static,
317>;
318
319#[cfg(feature = "test-support")]
320pub struct FakeHttpClient {
321 handler: FakeHttpHandler,
322}
323
324#[cfg(feature = "test-support")]
325impl FakeHttpClient {
326 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
327 where
328 Fut: futures::Future<Output = Result<Response<AsyncBody>, anyhow::Error>> + Send + 'static,
329 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
330 {
331 Arc::new(HttpClientWithUrl {
332 base_url: Mutex::new("http://test.example".into()),
333 client: HttpClientWithProxy {
334 client: Arc::new(Self {
335 handler: Box::new(move |req| Box::pin(handler(req))),
336 }),
337 proxy: None,
338 },
339 })
340 }
341
342 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
343 Self::create(|_| async move {
344 Ok(Response::builder()
345 .status(404)
346 .body(Default::default())
347 .unwrap())
348 })
349 }
350
351 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
352 Self::create(|_| async move {
353 Ok(Response::builder()
354 .status(200)
355 .body(Default::default())
356 .unwrap())
357 })
358 }
359}
360
361#[cfg(feature = "test-support")]
362impl fmt::Debug for FakeHttpClient {
363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364 f.debug_struct("FakeHttpClient").finish()
365 }
366}
367
368#[cfg(feature = "test-support")]
369impl HttpClient for FakeHttpClient {
370 fn send(
371 &self,
372 req: Request<AsyncBody>,
373 ) -> BoxFuture<'static, Result<Response<AsyncBody>, anyhow::Error>> {
374 let future = (self.handler)(req);
375 future
376 }
377
378 fn proxy(&self) -> Option<&Uri> {
379 None
380 }
381}