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