1use crate::http_proxy_from_env;
2pub use anyhow::{anyhow, Result};
3use futures::future::BoxFuture;
4use futures_lite::FutureExt;
5use isahc::config::{Configurable, RedirectPolicy};
6pub use isahc::{
7 http::{Method, StatusCode, Uri},
8 AsyncBody, Error, HttpClient as IsahcHttpClient, Request, Response,
9};
10#[cfg(feature = "test-support")]
11use std::fmt;
12use std::{
13 sync::{Arc, Mutex},
14 time::Duration,
15};
16pub use url::Url;
17
18/// An [`HttpClient`] that has a base URL.
19pub struct HttpClientWithUrl {
20 base_url: Mutex<String>,
21 client: Arc<dyn HttpClient>,
22}
23
24impl HttpClientWithUrl {
25 /// Returns a new [`HttpClientWithUrl`] with the given base URL.
26 pub fn new(base_url: impl Into<String>) -> Self {
27 Self {
28 base_url: Mutex::new(base_url.into()),
29 client: client(),
30 }
31 }
32
33 /// Returns the base URL.
34 pub fn base_url(&self) -> String {
35 self.base_url
36 .lock()
37 .map_or_else(|_| Default::default(), |url| url.clone())
38 }
39
40 /// Sets the base URL.
41 pub fn set_base_url(&self, base_url: impl Into<String>) {
42 let base_url = base_url.into();
43 self.base_url
44 .lock()
45 .map(|mut url| {
46 *url = base_url;
47 })
48 .ok();
49 }
50
51 /// Builds a URL using the given path.
52 pub fn build_url(&self, path: &str) -> String {
53 format!("{}{}", self.base_url(), path)
54 }
55
56 /// Builds a Zed API URL using the given path.
57 pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
58 let base_url = self.base_url();
59 let base_api_url = match base_url.as_ref() {
60 "https://zed.dev" => "https://api.zed.dev",
61 "https://staging.zed.dev" => "https://api-staging.zed.dev",
62 "http://localhost:3000" => "http://localhost:8080",
63 other => other,
64 };
65
66 Ok(Url::parse_with_params(
67 &format!("{}{}", base_api_url, path),
68 query,
69 )?)
70 }
71}
72
73impl HttpClient for Arc<HttpClientWithUrl> {
74 fn send(
75 &self,
76 req: Request<AsyncBody>,
77 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
78 self.client.send(req)
79 }
80}
81
82impl HttpClient for HttpClientWithUrl {
83 fn send(
84 &self,
85 req: Request<AsyncBody>,
86 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
87 self.client.send(req)
88 }
89}
90
91pub trait HttpClient: Send + Sync {
92 fn send(
93 &self,
94 req: Request<AsyncBody>,
95 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
96
97 fn get<'a>(
98 &'a self,
99 uri: &str,
100 body: AsyncBody,
101 follow_redirects: bool,
102 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
103 let request = isahc::Request::builder()
104 .redirect_policy(if follow_redirects {
105 RedirectPolicy::Follow
106 } else {
107 RedirectPolicy::None
108 })
109 .method(Method::GET)
110 .uri(uri)
111 .body(body);
112 match request {
113 Ok(request) => self.send(request),
114 Err(error) => async move { Err(error.into()) }.boxed(),
115 }
116 }
117
118 fn post_json<'a>(
119 &'a self,
120 uri: &str,
121 body: AsyncBody,
122 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
123 let request = isahc::Request::builder()
124 .method(Method::POST)
125 .uri(uri)
126 .header("Content-Type", "application/json")
127 .body(body);
128 match request {
129 Ok(request) => self.send(request),
130 Err(error) => async move { Err(error.into()) }.boxed(),
131 }
132 }
133}
134
135pub fn client() -> Arc<dyn HttpClient> {
136 Arc::new(
137 isahc::HttpClient::builder()
138 .connect_timeout(Duration::from_secs(5))
139 .low_speed_timeout(100, Duration::from_secs(5))
140 .proxy(http_proxy_from_env())
141 .build()
142 .unwrap(),
143 )
144}
145
146impl HttpClient for isahc::HttpClient {
147 fn send(
148 &self,
149 req: Request<AsyncBody>,
150 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
151 let client = self.clone();
152 Box::pin(async move { client.send_async(req).await })
153 }
154}
155
156#[cfg(feature = "test-support")]
157type FakeHttpHandler = Box<
158 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
159 + Send
160 + Sync
161 + 'static,
162>;
163
164#[cfg(feature = "test-support")]
165pub struct FakeHttpClient {
166 handler: FakeHttpHandler,
167}
168
169#[cfg(feature = "test-support")]
170impl FakeHttpClient {
171 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
172 where
173 Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
174 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
175 {
176 Arc::new(HttpClientWithUrl {
177 base_url: Mutex::new("http://test.example".into()),
178 client: Arc::new(Self {
179 handler: Box::new(move |req| Box::pin(handler(req))),
180 }),
181 })
182 }
183
184 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
185 Self::create(|_| async move {
186 Ok(Response::builder()
187 .status(404)
188 .body(Default::default())
189 .unwrap())
190 })
191 }
192
193 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
194 Self::create(|_| async move {
195 Ok(Response::builder()
196 .status(200)
197 .body(Default::default())
198 .unwrap())
199 })
200 }
201}
202
203#[cfg(feature = "test-support")]
204impl fmt::Debug for FakeHttpClient {
205 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206 f.debug_struct("FakeHttpClient").finish()
207 }
208}
209
210#[cfg(feature = "test-support")]
211impl HttpClient for FakeHttpClient {
212 fn send(
213 &self,
214 req: Request<AsyncBody>,
215 ) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
216 let future = (self.handler)(req);
217 Box::pin(async move { future.await.map(Into::into) })
218 }
219}