1pub mod github;
2
3pub use anyhow::{anyhow, Result};
4use futures::future::BoxFuture;
5use futures_lite::FutureExt;
6use isahc::config::{Configurable, RedirectPolicy};
7pub use isahc::{
8 http::{Method, StatusCode, Uri},
9 AsyncBody, Error, HttpClient as IsahcHttpClient, Request, Response,
10};
11#[cfg(feature = "test-support")]
12use std::fmt;
13use std::{
14 sync::{Arc, Mutex},
15 time::Duration,
16};
17pub use url::Url;
18
19fn get_proxy(proxy: Option<String>) -> Option<isahc::http::Uri> {
20 macro_rules! try_env {
21 ($($env:literal),+) => {
22 $(
23 if let Ok(env) = std::env::var($env) {
24 return env.parse::<isahc::http::Uri>().ok();
25 }
26 )+
27 };
28 }
29
30 proxy
31 .and_then(|input| {
32 input
33 .parse::<isahc::http::Uri>()
34 .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
35 .ok()
36 })
37 .or_else(|| {
38 try_env!(
39 "ALL_PROXY",
40 "all_proxy",
41 "HTTPS_PROXY",
42 "https_proxy",
43 "HTTP_PROXY",
44 "http_proxy"
45 );
46 None
47 })
48}
49
50/// An [`HttpClient`] that has a base URL.
51pub struct HttpClientWithUrl {
52 base_url: Mutex<String>,
53 client: Arc<dyn HttpClient>,
54 proxy: Option<String>,
55}
56
57impl HttpClientWithUrl {
58 /// Returns a new [`HttpClientWithUrl`] with the given base URL.
59 pub fn new(base_url: impl Into<String>, unparsed_proxy: Option<String>) -> Self {
60 let parsed_proxy = get_proxy(unparsed_proxy);
61 let proxy_string = parsed_proxy.as_ref().map(|p| {
62 // Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809`
63 // NodeRuntime without environment information can not parse `localhost`
64 // correctly.
65 // TODO: map to `[::1]` if we are using ipv6
66 p.to_string()
67 .to_ascii_lowercase()
68 .replace("localhost", "127.0.0.1")
69 });
70 Self {
71 base_url: Mutex::new(base_url.into()),
72 client: client(parsed_proxy),
73 proxy: proxy_string,
74 }
75 }
76
77 /// Returns the base URL.
78 pub fn base_url(&self) -> String {
79 self.base_url
80 .lock()
81 .map_or_else(|_| Default::default(), |url| url.clone())
82 }
83
84 /// Sets the base URL.
85 pub fn set_base_url(&self, base_url: impl Into<String>) {
86 let base_url = base_url.into();
87 self.base_url
88 .lock()
89 .map(|mut url| {
90 *url = base_url;
91 })
92 .ok();
93 }
94
95 /// Builds a URL using the given path.
96 pub fn build_url(&self, path: &str) -> String {
97 format!("{}{}", self.base_url(), path)
98 }
99
100 /// Builds a Zed API URL using the given path.
101 pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
102 let base_url = self.base_url();
103 let base_api_url = match base_url.as_ref() {
104 "https://zed.dev" => "https://api.zed.dev",
105 "https://staging.zed.dev" => "https://api-staging.zed.dev",
106 "http://localhost:3000" => "http://localhost:8080",
107 other => other,
108 };
109
110 Ok(Url::parse_with_params(
111 &format!("{}{}", base_api_url, path),
112 query,
113 )?)
114 }
115}
116
117impl HttpClient for Arc<HttpClientWithUrl> {
118 fn send(
119 &self,
120 req: Request<AsyncBody>,
121 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
122 self.client.send(req)
123 }
124
125 fn proxy(&self) -> Option<&str> {
126 self.proxy.as_deref()
127 }
128}
129
130impl HttpClient for HttpClientWithUrl {
131 fn send(
132 &self,
133 req: Request<AsyncBody>,
134 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
135 self.client.send(req)
136 }
137
138 fn proxy(&self) -> Option<&str> {
139 self.proxy.as_deref()
140 }
141}
142
143pub trait HttpClient: Send + Sync {
144 fn send(
145 &self,
146 req: Request<AsyncBody>,
147 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
148
149 fn get<'a>(
150 &'a self,
151 uri: &str,
152 body: AsyncBody,
153 follow_redirects: bool,
154 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
155 let request = isahc::Request::builder()
156 .redirect_policy(if follow_redirects {
157 RedirectPolicy::Follow
158 } else {
159 RedirectPolicy::None
160 })
161 .method(Method::GET)
162 .uri(uri)
163 .body(body);
164 match request {
165 Ok(request) => self.send(request),
166 Err(error) => async move { Err(error.into()) }.boxed(),
167 }
168 }
169
170 fn post_json<'a>(
171 &'a self,
172 uri: &str,
173 body: AsyncBody,
174 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
175 let request = isahc::Request::builder()
176 .method(Method::POST)
177 .uri(uri)
178 .header("Content-Type", "application/json")
179 .body(body);
180 match request {
181 Ok(request) => self.send(request),
182 Err(error) => async move { Err(error.into()) }.boxed(),
183 }
184 }
185
186 fn proxy(&self) -> Option<&str>;
187}
188
189pub fn client(proxy: Option<isahc::http::Uri>) -> Arc<dyn HttpClient> {
190 Arc::new(
191 isahc::HttpClient::builder()
192 .connect_timeout(Duration::from_secs(5))
193 .low_speed_timeout(100, Duration::from_secs(5))
194 .proxy(proxy)
195 .build()
196 .unwrap(),
197 )
198}
199
200impl HttpClient for isahc::HttpClient {
201 fn send(
202 &self,
203 req: Request<AsyncBody>,
204 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
205 let client = self.clone();
206 Box::pin(async move { client.send_async(req).await })
207 }
208
209 fn proxy(&self) -> Option<&str> {
210 None
211 }
212}
213
214#[cfg(feature = "test-support")]
215type FakeHttpHandler = Box<
216 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
217 + Send
218 + Sync
219 + 'static,
220>;
221
222#[cfg(feature = "test-support")]
223pub struct FakeHttpClient {
224 handler: FakeHttpHandler,
225}
226
227#[cfg(feature = "test-support")]
228impl FakeHttpClient {
229 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
230 where
231 Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
232 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
233 {
234 Arc::new(HttpClientWithUrl {
235 base_url: Mutex::new("http://test.example".into()),
236 client: Arc::new(Self {
237 handler: Box::new(move |req| Box::pin(handler(req))),
238 }),
239 proxy: None,
240 })
241 }
242
243 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
244 Self::create(|_| async move {
245 Ok(Response::builder()
246 .status(404)
247 .body(Default::default())
248 .unwrap())
249 })
250 }
251
252 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
253 Self::create(|_| async move {
254 Ok(Response::builder()
255 .status(200)
256 .body(Default::default())
257 .unwrap())
258 })
259 }
260}
261
262#[cfg(feature = "test-support")]
263impl fmt::Debug for FakeHttpClient {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 f.debug_struct("FakeHttpClient").finish()
266 }
267}
268
269#[cfg(feature = "test-support")]
270impl HttpClient for FakeHttpClient {
271 fn send(
272 &self,
273 req: Request<AsyncBody>,
274 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
275 let future = (self.handler)(req);
276 Box::pin(async move { future.await.map(Into::into) })
277 }
278
279 fn proxy(&self) -> Option<&str> {
280 None
281 }
282}