v_exchanges_api_generics/
http.rs

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