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(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
75 self.client.send(req)
76 }
77}
78
79impl HttpClient for HttpClientWithUrl {
80 fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
81 self.client.send(req)
82 }
83}
84
85pub trait HttpClient: Send + Sync {
86 fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>>;
87
88 fn get<'a>(
89 &'a self,
90 uri: &str,
91 body: AsyncBody,
92 follow_redirects: bool,
93 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
94 let request = isahc::Request::builder()
95 .redirect_policy(if follow_redirects {
96 RedirectPolicy::Follow
97 } else {
98 RedirectPolicy::None
99 })
100 .method(Method::GET)
101 .uri(uri)
102 .body(body);
103 match request {
104 Ok(request) => self.send(request),
105 Err(error) => async move { Err(error.into()) }.boxed(),
106 }
107 }
108
109 fn post_json<'a>(
110 &'a self,
111 uri: &str,
112 body: AsyncBody,
113 ) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
114 let request = isahc::Request::builder()
115 .method(Method::POST)
116 .uri(uri)
117 .header("Content-Type", "application/json")
118 .body(body);
119 match request {
120 Ok(request) => self.send(request),
121 Err(error) => async move { Err(error.into()) }.boxed(),
122 }
123 }
124}
125
126pub fn client() -> Arc<dyn HttpClient> {
127 Arc::new(
128 isahc::HttpClient::builder()
129 .connect_timeout(Duration::from_secs(5))
130 .low_speed_timeout(100, Duration::from_secs(5))
131 .proxy(http_proxy_from_env())
132 .build()
133 .unwrap(),
134 )
135}
136
137impl HttpClient for isahc::HttpClient {
138 fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
139 Box::pin(async move { self.send_async(req).await })
140 }
141}
142
143#[cfg(feature = "test-support")]
144type FakeHttpHandler = Box<
145 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
146 + Send
147 + Sync
148 + 'static,
149>;
150
151#[cfg(feature = "test-support")]
152pub struct FakeHttpClient {
153 handler: FakeHttpHandler,
154}
155
156#[cfg(feature = "test-support")]
157impl FakeHttpClient {
158 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
159 where
160 Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
161 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
162 {
163 Arc::new(HttpClientWithUrl {
164 base_url: Mutex::new("http://test.example".into()),
165 client: Arc::new(Self {
166 handler: Box::new(move |req| Box::pin(handler(req))),
167 }),
168 })
169 }
170
171 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
172 Self::create(|_| async move {
173 Ok(Response::builder()
174 .status(404)
175 .body(Default::default())
176 .unwrap())
177 })
178 }
179
180 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
181 Self::create(|_| async move {
182 Ok(Response::builder()
183 .status(200)
184 .body(Default::default())
185 .unwrap())
186 })
187 }
188}
189
190#[cfg(feature = "test-support")]
191impl fmt::Debug for FakeHttpClient {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 f.debug_struct("FakeHttpClient").finish()
194 }
195}
196
197#[cfg(feature = "test-support")]
198impl HttpClient for FakeHttpClient {
199 fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
200 let future = (self.handler)(req);
201 Box::pin(async move { future.await.map(Into::into) })
202 }
203}