v_exchanges_api_generics/
http.rs

1use std::{fmt::Debug, path::PathBuf, sync::OnceLock, time::Duration};
2
3pub use bytes::Bytes;
4use jiff::Timestamp;
5use reqwest::Url;
6pub use reqwest::{
7	Method, Request, RequestBuilder, StatusCode,
8	header::{self, HeaderMap},
9};
10use v_utils::{prelude::*, xdg_cache_dir};
11
12use crate::{AuthError, UrlError};
13
14/// The User Agent string
15pub static USER_AGENT: &str = concat!("v_exchanges_api_generics/", env!("CARGO_PKG_VERSION"));
16
17/// Client for communicating with APIs through HTTP/HTTPS.
18///
19/// When making a HTTP request or starting a websocket connection with this client,
20/// a handler that implements [RequestHandler] is required.
21#[derive(Clone, Debug, Default)]
22pub struct Client {
23	client: reqwest::Client,
24	pub config: RequestConfig,
25}
26
27impl Client {
28	/// Makes an HTTP request with the given [RequestHandler] and returns the response.
29	///
30	/// It is recommended to use methods like [get()][Self::get()] because this method takes many type parameters and parameters.
31	///
32	/// The request is passed to `handler` before being sent, and the response is passed to `handler` before being returned.
33	/// Note, that as stated in the docs for [RequestBuilder::query()], parameter `query` only accepts a **sequence of** key-value pairs.
34	#[instrument(skip_all, fields(?url, ?query, request_builder = Empty))] //TODO: get all generics to impl std::fmt::Debug
35	pub async fn request<Q, B, H>(&self, method: Method, url: &str, query: Option<&Q>, body: Option<B>, handler: &H) -> Result<H::Successful, RequestError>
36	where
37		Q: Serialize + ?Sized + std::fmt::Debug,
38		H: RequestHandler<B>, {
39		let config = &self.config;
40		config.verify();
41		let base_url = handler.base_url(config.use_testnet)?;
42		let url = base_url.join(url).map_err(|_| RequestError::Other(eyre!("Failed to parse provided URL")))?;
43		debug!(?config);
44
45		for i in 1..=config.max_tries {
46			//HACK: hate to create a new request every time, but I haven't yet figured out how to provide by reference
47			let mut request_builder = self.client.request(method.clone(), url.clone()).timeout(config.timeout);
48			if let Some(query) = query {
49				request_builder = request_builder.query(query);
50			}
51			Span::current().record("request_builder", format!("{request_builder:?}"));
52
53			if config.use_testnet
54				&& let Some(cache_duration) = config.cache_testnet_calls
55			{
56				let path = test_calls_path(&url, &query);
57				if let Ok(file) = std::fs::read_to_string(&path)
58					&& path
59						.metadata()
60						.expect("already read the file, guaranteed to exist")
61						.modified()
62						.expect("switch OSes, you're on something stupid")
63						.elapsed()
64						.unwrap() < cache_duration
65				{
66					let body = Bytes::from(file);
67					let (status, headers) = (StatusCode::OK, header::HeaderMap::new()); // we only cache if we get a 200 (headers are only relevant on unsuccessful), so pass defaults.
68					return handler.handle_response(status, headers, body).map_err(RequestError::HandleResponse);
69				}
70			}
71
72			//let (status, headers, truncated_body): (StatusCode, HeaderMap, String) = {
73			let request = handler.build_request(request_builder, &body, i).map_err(RequestError::BuildRequest)?;
74			match self.client.execute(request).await {
75				Ok(mut response) => {
76					let status = response.status();
77					let headers = std::mem::take(response.headers_mut());
78					let body: Bytes = response.bytes().await.map_err(RequestError::ReceiveResponse)?;
79					{
80						let truncated_body = v_utils::utils::truncate_msg(std::str::from_utf8(&body)?.trim());
81						debug!(truncated_body);
82					}
83
84					match config.use_testnet {
85						true => {
86							// if we're here, the cache file didn't exist or is outdated
87							let handled = handler.handle_response(status, headers.clone(), body.clone())?;
88							std::fs::write(test_calls_path(&url, &query), &body).ok();
89							return Ok(handled);
90						}
91						false => {
92							return handler.handle_response(status, headers, body).map_err(RequestError::HandleResponse);
93						}
94					}
95				}
96				Err(e) =>
97				//TODO!!!: we are only retrying when response is not received. Although there is a list of errors we would also like to retry on.
98					if i < config.max_tries && e.is_timeout() {
99						info!("Retrying sending request; made so far: {i}");
100						tokio::time::sleep(config.retry_cooldown).await;
101					} else {
102						warn!(?e);
103						debug!("{:?}\nAnd then trying the .is_timeout(): {}", e.status(), e.is_timeout());
104						return Err(RequestError::SendRequest(e));
105					},
106			}
107		}
108
109		unreachable!()
110	}
111
112	/// Makes an GET request with the given [RequestHandler].
113	///
114	/// This method just calls [request()][Self::request()]. It requires less typing for type parameters and parameters.
115	/// This method requires that `handler` can handle a request with a body of type `()`. The actual body passed will be `None`.
116	///
117	/// For more information, see [request()][Self::request()].
118	pub async fn get<Q, H>(&self, url: &str, query: &Q, handler: &H) -> Result<H::Successful, RequestError>
119	where
120		Q: Serialize + ?Sized + Debug,
121		H: RequestHandler<()>, {
122		self.request::<Q, (), H>(Method::GET, url, Some(query), None, handler).await
123	}
124
125	/// Derivation of [get()][Self::get()].
126	pub async fn get_no_query<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
127	where
128		H: RequestHandler<()>, {
129		self.request::<&[(&str, &str)], (), H>(Method::GET, url, None, None, handler).await
130	}
131
132	/// Makes an POST request with the given [RequestHandler].
133	///
134	/// This method just calls [request()][Self::request()]. It requires less typing for type parameters and parameters.
135	///
136	/// For more information, see [request()][Self::request()].
137	pub async fn post<B, H>(&self, url: &str, body: B, handler: &H) -> Result<H::Successful, RequestError>
138	where
139		H: RequestHandler<B>, {
140		self.request::<(), B, H>(Method::POST, url, None, Some(body), handler).await
141	}
142
143	/// Derivation of [post()][Self::post()].
144	pub async fn post_no_body<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
145	where
146		H: RequestHandler<()>, {
147		self.request::<(), (), H>(Method::POST, url, None, None, handler).await
148	}
149
150	/// Makes an PUT request with the given [RequestHandler].
151	///
152	/// This method just calls [request()][Self::request()]. It requires less typing for type parameters and parameters.
153	///
154	/// For more information, see [request()][Self::request()].
155	pub async fn put<B, H>(&self, url: &str, body: B, handler: &H) -> Result<H::Successful, RequestError>
156	where
157		H: RequestHandler<B>, {
158		self.request::<(), B, H>(Method::PUT, url, None, Some(body), handler).await
159	}
160
161	/// Derivation of [put()][Self::put()].
162	pub async fn put_no_body<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
163	where
164		H: RequestHandler<()>, {
165		self.request::<(), (), H>(Method::PUT, url, None, None, handler).await
166	}
167
168	/// Makes an DELETE request with the given [RequestHandler].
169	///
170	/// This method just calls [request()][Self::request()]. It requires less typing for type parameters and parameters.
171	/// This method requires that `handler` can handle a request with a body of type `()`. The actual body passed will be `None`.
172	///
173	/// For more information, see [request()][Self::request()].
174	pub async fn delete<Q, H>(&self, url: &str, query: &Q, handler: &H) -> Result<H::Successful, RequestError>
175	where
176		Q: Serialize + ?Sized + Debug,
177		H: RequestHandler<()>, {
178		self.request::<Q, (), H>(Method::DELETE, url, Some(query), None, handler).await
179	}
180
181	/// Derivation of [delete()][Self::delete()].
182	pub async fn delete_no_query<H>(&self, url: &str, handler: &H) -> Result<H::Successful, RequestError>
183	where
184		H: RequestHandler<()>, {
185		self.request::<&[(&str, &str)], (), H>(Method::DELETE, url, None, None, handler).await
186	}
187}
188
189/// A `trait` which is used to process requests and responses for the [Client].
190pub trait RequestHandler<B> {
191	/// The type which is returned to the caller of [Client::request()] when the response was successful.
192	type Successful;
193
194	/// Produce a url prefix (if any).
195	#[allow(unused_variables)]
196	fn base_url(&self, is_test: bool) -> Result<url::Url, UrlError> {
197		Url::parse("").map_err(UrlError::Parse)
198	}
199
200	/// Build a HTTP request to be sent.
201	///
202	/// Implementors have to decide how to include the `request_body` into the `builder`. Implementors can
203	/// also perform other operations (such as authorization) on the request.
204	fn build_request(&self, builder: RequestBuilder, request_body: &Option<B>, attempt_count: u8) -> Result<Request, BuildError>;
205
206	/// Handle a HTTP response before it is returned to the caller of [Client::request()].
207	///
208	/// You can verify, parse, etc... the response here before it is returned to the caller.
209	///
210	/// # Examples
211	/// ```
212	/// # use bytes::Bytes;
213	/// # use reqwest::{StatusCode, header::HeaderMap};
214	/// # trait Ignore {
215	/// fn handle_response(&self, status: StatusCode, _: HeaderMap, response_body: Bytes) -> Result<String, ()> {
216	///     if status.is_success() {
217	///         let body = std::str::from_utf8(&response_body).expect("body should be valid UTF-8").to_owned();
218	///         Ok(body)
219	///     } else {
220	///         Err(())
221	///     }
222	/// }
223	/// # }
224	/// ```
225	fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes) -> Result<Self::Successful, HandleError>;
226}
227
228/// Configuration when sending a request using [Client].
229///
230/// Modified in-place later if necessary.
231#[derive(Clone, Debug, Default)]
232pub struct RequestConfig {
233	/// [Client] will retry sending a request if it failed to send. `max_try` can be used limit the number of attempts.
234	///
235	/// Do not set this to `0` or [Client::request()] will **panic**. [Default]s to `1` (which means no retry).
236	//TODO: change to `num_retries`, so there is no special case.
237	pub max_tries: u8 = 1,
238	/// Duration that should elapse after retrying sending a request.
239	pub retry_cooldown: Duration = Duration::from_millis(500),
240	/// The timeout set when sending a request. [Default]s to 3s.
241	///
242	/// It is possible for the [RequestHandler] to override this in [RequestHandler::build_request()].
243	/// See also: [RequestBuilder::timeout()].
244	pub timeout: Duration = Duration::from_secs(3),
245
246	/// Make all requests in test mode
247	pub use_testnet: bool,
248	/// if `test` is true, then we will try to read the file with the cached result of any request to the same URL, aged less than specified [Duration]
249	pub cache_testnet_calls: Option<Duration> = Some(Duration::from_days(30)),
250}
251
252impl RequestConfig {
253	fn verify(&self) {
254		assert_ne!(self.max_tries, 0, "RequestConfig.max_tries must not be equal to 0");
255	}
256}
257
258/// Error type encompassing all the failure modes of [RequestHandler::handle_response()].
259#[derive(Debug, derive_more::Display, Error, derive_more::From)]
260pub enum HandleError {
261	/// Refer to [ApiError]
262	Api(ApiError),
263	/// Couldn't parse the response. Most often will wrap a [serde_json::Error].
264	Parse(serde_json::Error),
265	#[allow(missing_docs)]
266	Other(Report),
267}
268/// Errors that exchanges purposefully transmit.
269#[derive(Debug, Error, derive_more::From)]
270pub enum ApiError {
271	/// Ip has been timed out or banned
272	#[error("IP has been timed out or banned until {until:?}")]
273	IpTimeout {
274		/// Time of unban
275		until: Option<Timestamp>,
276	},
277	/// Errors that are a) specific to a particular exchange or b) should be handled by this crate, but are here for dev convenience
278	#[error("{0}")]
279	Other(Report),
280}
281
282/// An `enum` that represents errors that could be returned by [Client::request()]
283#[derive(Debug, thiserror::Error)]
284pub enum RequestError {
285	#[error("failed to send HTTP request: {0}")]
286	SendRequest(#[source] reqwest::Error),
287	#[error("failed to parse response body as UTF-8: {0}")]
288	Utf8Error(#[from] std::str::Utf8Error),
289	#[error("failed to receive HTTP response: {0}")]
290	ReceiveResponse(#[source] reqwest::Error),
291	#[error("handler failed to build a request: {0}")]
292	BuildRequest(#[from] BuildError),
293	#[error("handler failed to process the response: {0}")]
294	HandleResponse(#[from] HandleError),
295	#[error("{0}")]
296	Url(#[from] UrlError),
297	/// errors meant to be propagated to the user or the developer, thus having no defined type.
298	#[allow(missing_docs)]
299	#[error("{0}")]
300	Other(#[from] Report),
301}
302
303/// Errors that can occur during exchange's implementation of the build-request process.
304#[derive(Debug, derive_more::Display, thiserror::Error, derive_more::From)]
305pub enum BuildError {
306	/// Signed request attempted, while lacking one of the necessary auth fields
307	Auth(AuthError),
308	/// Could not serialize body as application/x-www-form-urlencoded
309	UrlSerialization(serde_urlencoded::ser::Error),
310	/// Could not serialize body as application/json
311	JsonSerialization(serde_json::Error),
312	//Q: not sure if there is ever a case when client could reach that, thus currently simply unwraping.
313	///// Error when calling reqwest::RequestBuilder::build()
314	//Reqwest(reqwest::Error),
315	#[allow(missing_docs)]
316	Other(Report),
317}
318
319static TEST_CALLS_PATH: OnceLock<PathBuf> = OnceLock::new();
320fn test_calls_path<Q: Serialize>(url: &Url, query: &Option<Q>) -> PathBuf {
321	let base = TEST_CALLS_PATH.get_or_init(|| xdg_cache_dir!("test_calls"));
322
323	let mut filename = url.to_string();
324	if query.is_some() {
325		filename.push('?');
326		filename.push_str(&serde_urlencoded::to_string(query).unwrap_or_default());
327	}
328	base.join(filename)
329}