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")]
132type FakeHttpHandler = Box<
133 dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>
134 + Send
135 + Sync
136 + 'static,
137>;
138
139#[cfg(feature = "test-support")]
140pub struct FakeHttpClient {
141 handler: FakeHttpHandler,
142}
143
144#[cfg(feature = "test-support")]
145impl FakeHttpClient {
146 pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
147 where
148 Fut: futures::Future<Output = Result<Response<AsyncBody>, Error>> + Send + 'static,
149 F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
150 {
151 Arc::new(HttpClientWithUrl {
152 base_url: Mutex::new("http://test.example".into()),
153 client: Arc::new(Self {
154 handler: Box::new(move |req| Box::pin(handler(req))),
155 }),
156 })
157 }
158
159 pub fn with_404_response() -> Arc<HttpClientWithUrl> {
160 Self::create(|_| async move {
161 Ok(Response::builder()
162 .status(404)
163 .body(Default::default())
164 .unwrap())
165 })
166 }
167
168 pub fn with_200_response() -> Arc<HttpClientWithUrl> {
169 Self::create(|_| async move {
170 Ok(Response::builder()
171 .status(200)
172 .body(Default::default())
173 .unwrap())
174 })
175 }
176}
177
178#[cfg(feature = "test-support")]
179impl fmt::Debug for FakeHttpClient {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 f.debug_struct("FakeHttpClient").finish()
182 }
183}
184
185#[cfg(feature = "test-support")]
186impl HttpClient for FakeHttpClient {
187 fn send(&self, req: Request<AsyncBody>) -> BoxFuture<Result<Response<AsyncBody>, Error>> {
188 let future = (self.handler)(req);
189 Box::pin(async move { future.await.map(Into::into) })
190 }
191}