Skip to content

Commit 999d1b0

Browse files
committed
refactor(client)!: use bitreq for AsyncClient
- update the `AsyncClient` to use `bitreq` instead of `reqwest`
1 parent 000bf67 commit 999d1b0

File tree

3 files changed

+119
-107
lines changed

3 files changed

+119
-107
lines changed

Cargo.toml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ serde_json = { version = "1.0", default-features = false }
2525
bitcoin = { version = "0.32", features = ["serde", "std"], default-features = false }
2626
hex = { version = "0.2", package = "hex-conservative" }
2727
log = "^0.4"
28-
reqwest = { version = "0.12", features = ["json"], default-features = false, optional = true }
2928
bitreq = { version = "0.3.4", optional = true }
3029

3130
# default async runtime
@@ -37,16 +36,16 @@ electrsd = { version = "0.36.1", features = ["legacy", "esplora_a33e97e1", "core
3736
lazy_static = "1.4.0"
3837

3938
[features]
40-
default = ["blocking", "async", "async-https", "tokio"]
41-
blocking = ["bitreq", "bitreq/proxy", "bitreq/json-using-serde"]
39+
default = ["blocking", "blocking-https", "async", "async-https", "tokio"]
40+
blocking = ["bitreq/proxy", "bitreq/json-using-serde"]
4241
blocking-https = ["blocking", "bitreq/https"]
4342
blocking-https-native = ["blocking", "bitreq/https-native-tls"]
4443
blocking-https-rustls = ["blocking", "bitreq/https-rustls"]
4544
blocking-https-rustls-probe = ["blocking", "bitreq/https-rustls-probe"]
4645

4746
tokio = ["dep:tokio"]
48-
async = ["reqwest", "reqwest/socks", "tokio?/time"]
49-
async-https = ["async", "reqwest/default-tls"]
50-
async-https-native = ["async", "reqwest/native-tls"]
51-
async-https-rustls = ["async", "reqwest/rustls-tls"]
52-
async-https-rustls-manual-roots = ["async", "reqwest/rustls-tls-manual-roots"]
47+
async = ["bitreq/async", "bitreq/proxy", "bitreq/json-using-serde", "tokio?/time"]
48+
async-https = ["async", "bitreq/async-https"]
49+
async-https-native = ["async", "bitreq/async-https-native-tls"]
50+
async-https-rustls = ["async", "bitreq/async-https-rustls"]
51+
async-https-rustls-probe = ["async", "bitreq/async-https-rustls-probe"]

src/async.rs

Lines changed: 99 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ use bitcoin::hashes::{sha256, Hash};
2323
use bitcoin::hex::{DisplayHex, FromHex};
2424
use bitcoin::{Address, Block, BlockHash, MerkleBlock, Script, Transaction, Txid};
2525

26-
#[allow(unused_imports)]
27-
use log::{debug, error, info, trace};
28-
29-
use reqwest::{header, Body, Client, Response};
26+
use bitreq::{Client, RequestExt, Response};
3027

3128
use crate::{
3229
AddressStats, BlockInfo, BlockStatus, BlockSummary, Builder, Error, MempoolRecentTx,
@@ -35,61 +32,41 @@ use crate::{
3532
};
3633

3734
/// An async client for interacting with an Esplora API server.
38-
#[derive(Debug, Clone)]
35+
// FIXME: (@oleonardolima) there's no `Debug` implementation for `bitreq::Client`.
36+
#[derive(Clone)]
3937
pub struct AsyncClient<S = DefaultSleeper> {
4038
/// The URL of the Esplora Server.
4139
url: String,
42-
/// The inner [`reqwest::Client`] to make HTTP requests.
43-
client: Client,
40+
/// The proxy is ignored when targeting `wasm32`.
41+
proxy: Option<String>,
42+
/// Socket timeout.
43+
timeout: Option<u64>,
44+
/// HTTP headers to set on every request made to Esplora server
45+
headers: HashMap<String, String>,
4446
/// Number of times to retry a request
4547
max_retries: usize,
48+
/// The inner [`reqwest::Client`] to make HTTP requests.
49+
client: Client,
4650
/// Marker for the type of sleeper used
4751
marker: PhantomData<S>,
4852
}
4953

5054
impl<S: Sleeper> AsyncClient<S> {
5155
/// Build an [`AsyncClient`] from a [`Builder`].
5256
pub fn from_builder(builder: Builder) -> Result<Self, Error> {
53-
let mut client_builder = Client::builder();
54-
55-
#[cfg(not(target_arch = "wasm32"))]
56-
if let Some(proxy) = &builder.proxy {
57-
client_builder = client_builder.proxy(reqwest::Proxy::all(proxy)?);
58-
}
59-
60-
#[cfg(not(target_arch = "wasm32"))]
61-
if let Some(timeout) = builder.timeout {
62-
client_builder = client_builder.timeout(core::time::Duration::from_secs(timeout));
63-
}
64-
65-
if !builder.headers.is_empty() {
66-
let mut headers = header::HeaderMap::new();
67-
for (k, v) in builder.headers {
68-
let header_name = header::HeaderName::from_lowercase(k.to_lowercase().as_bytes())
69-
.map_err(|_| Error::InvalidHttpHeaderName(k))?;
70-
let header_value = header::HeaderValue::from_str(&v)
71-
.map_err(|_| Error::InvalidHttpHeaderValue(v))?;
72-
headers.insert(header_name, header_value);
73-
}
74-
client_builder = client_builder.default_headers(headers);
75-
}
57+
// TODO: (@oleonardolima) we should expose this to the final user through `Builder`.
58+
let cached_connections = 10;
59+
let client = Client::new(cached_connections);
7660

7761
Ok(AsyncClient {
7862
url: builder.base_url,
79-
client: client_builder.build()?,
63+
proxy: builder.proxy,
64+
timeout: builder.timeout,
65+
headers: builder.headers,
8066
max_retries: builder.max_retries,
81-
marker: PhantomData,
82-
})
83-
}
84-
85-
/// Build an [`AsyncClient`] from a [`Client`].
86-
pub fn from_client(url: String, client: Client) -> Self {
87-
AsyncClient {
88-
url,
8967
client,
90-
max_retries: crate::DEFAULT_MAX_RETRIES,
9168
marker: PhantomData,
92-
}
69+
})
9370
}
9471

9572
/// Make an HTTP GET request to given URL, deserializing to any `T` that
@@ -107,14 +84,13 @@ impl<S: Sleeper> AsyncClient<S> {
10784
let url = format!("{}{}", self.url, path);
10885
let response = self.get_with_retry(&url).await?;
10986

110-
if !response.status().is_success() {
111-
return Err(Error::HttpResponse {
112-
status: response.status().as_u16(),
113-
message: response.text().await?,
114-
});
87+
if !is_success(&response) {
88+
let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
89+
let message = response.as_str().unwrap_or_default().to_string();
90+
return Err(Error::HttpResponse { status, message });
11591
}
11692

117-
Ok(deserialize::<T>(&response.bytes().await?)?)
93+
Ok(deserialize::<T>(response.as_bytes())?)
11894
}
11995

12096
/// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
@@ -147,14 +123,13 @@ impl<S: Sleeper> AsyncClient<S> {
147123
let url = format!("{}{}", self.url, path);
148124
let response = self.get_with_retry(&url).await?;
149125

150-
if !response.status().is_success() {
151-
return Err(Error::HttpResponse {
152-
status: response.status().as_u16(),
153-
message: response.text().await?,
154-
});
126+
if !is_success(&response) {
127+
let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
128+
let message = response.as_str().unwrap_or_default().to_string();
129+
return Err(Error::HttpResponse { status, message });
155130
}
156131

157-
response.json::<T>().await.map_err(Error::Reqwest)
132+
response.json::<T>().map_err(Error::BitReq)
158133
}
159134

160135
/// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
@@ -189,15 +164,14 @@ impl<S: Sleeper> AsyncClient<S> {
189164
let url = format!("{}{}", self.url, path);
190165
let response = self.get_with_retry(&url).await?;
191166

192-
if !response.status().is_success() {
193-
return Err(Error::HttpResponse {
194-
status: response.status().as_u16(),
195-
message: response.text().await?,
196-
});
167+
if !is_success(&response) {
168+
let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
169+
let message = response.as_str().unwrap_or_default().to_string();
170+
return Err(Error::HttpResponse { status, message });
197171
}
198172

199-
let hex_str = response.text().await?;
200-
Ok(deserialize(&Vec::from_hex(&hex_str)?)?)
173+
let hex_str = response.as_str()?;
174+
Ok(deserialize(&Vec::from_hex(hex_str)?)?)
201175
}
202176

203177
/// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
@@ -226,14 +200,13 @@ impl<S: Sleeper> AsyncClient<S> {
226200
let url = format!("{}{}", self.url, path);
227201
let response = self.get_with_retry(&url).await?;
228202

229-
if !response.status().is_success() {
230-
return Err(Error::HttpResponse {
231-
status: response.status().as_u16(),
232-
message: response.text().await?,
233-
});
203+
if !is_success(&response) {
204+
let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
205+
let message = response.as_str().unwrap_or_default().to_string();
206+
return Err(Error::HttpResponse { status, message });
234207
}
235208

236-
Ok(response.text().await?)
209+
Ok(response.as_str()?.to_string())
237210
}
238211

239212
/// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
@@ -257,26 +230,25 @@ impl<S: Sleeper> AsyncClient<S> {
257230
///
258231
/// This function will return an error either from the HTTP client, or the
259232
/// response's [`serde_json`] deserialization.
260-
async fn post_request_bytes<T: Into<Body>>(
233+
async fn post_request_bytes<T: Into<Vec<u8>>>(
261234
&self,
262235
path: &str,
263236
body: T,
264237
query_params: Option<HashSet<(&str, String)>>,
265238
) -> Result<Response, Error> {
266239
let url: String = format!("{}{}", self.url, path);
267-
let mut request = self.client.post(url).body(body);
240+
let mut request: bitreq::Request = bitreq::post(url).with_body(body);
268241

269-
for param in query_params.unwrap_or_default() {
270-
request = request.query(&param);
242+
for (key, value) in query_params.unwrap_or_default() {
243+
request = request.with_param(key, value);
271244
}
272245

273-
let response = request.send().await?;
246+
let response = request.send_async_with_client(&self.client).await?;
274247

275-
if !response.status().is_success() {
276-
return Err(Error::HttpResponse {
277-
status: response.status().as_u16(),
278-
message: response.text().await?,
279-
});
248+
if !is_success(&response) {
249+
let status = u16::try_from(response.status_code).map_err(Error::StatusCode)?;
250+
let message = response.as_str().unwrap_or_default().to_string();
251+
return Err(Error::HttpResponse { status, message });
280252
}
281253

282254
Ok(response)
@@ -375,7 +347,7 @@ impl<S: Sleeper> AsyncClient<S> {
375347
pub async fn broadcast(&self, transaction: &Transaction) -> Result<Txid, Error> {
376348
let body = serialize::<Transaction>(transaction).to_lower_hex_string();
377349
let response = self.post_request_bytes("/tx", body, None).await?;
378-
let txid = Txid::from_str(&response.text().await?).map_err(Error::HexToArray)?;
350+
let txid = Txid::from_str(response.as_str()?).map_err(Error::HexToArray)?;
379351
Ok(txid)
380352
}
381353

@@ -414,7 +386,7 @@ impl<S: Sleeper> AsyncClient<S> {
414386
)
415387
.await?;
416388

417-
Ok(response.json::<SubmitPackageResult>().await?)
389+
Ok(response.json::<SubmitPackageResult>()?)
418390
}
419391

420392
/// Get the current height of the blockchain tip
@@ -606,21 +578,65 @@ impl<S: Sleeper> AsyncClient<S> {
606578
let mut delay = BASE_BACKOFF_MILLIS;
607579
let mut attempts = 0;
608580

581+
let mut request = bitreq::get(url);
582+
583+
#[cfg(not(target_arch = "wasm32"))]
584+
if let Some(proxy) = &self.proxy {
585+
use bitreq::Proxy;
586+
587+
let proxy = Proxy::new_http(proxy.as_str())?;
588+
request = request.with_proxy(proxy);
589+
}
590+
591+
#[cfg(not(target_arch = "wasm32"))]
592+
if let Some(timeout) = &self.timeout {
593+
request = request.with_timeout(*timeout);
594+
}
595+
596+
if !self.headers.is_empty() {
597+
request = request.with_headers(&self.headers);
598+
}
599+
609600
loop {
610-
match self.client.get(url).send().await? {
611-
resp if attempts < self.max_retries && is_status_retryable(resp.status()) => {
601+
match request.clone().send_async_with_client(&self.client).await? {
602+
response if attempts < self.max_retries && is_retryable(&response) => {
612603
S::sleep(delay).await;
613604
attempts += 1;
614605
delay *= 2;
615606
}
616-
resp => return Ok(resp),
607+
response => return Ok(response),
617608
}
618609
}
619610
}
620611
}
621612

622-
fn is_status_retryable(status: reqwest::StatusCode) -> bool {
623-
RETRYABLE_ERROR_CODES.contains(&status.as_u16())
613+
// /// Check if [`Response`] status is within 100-199.
614+
// fn is_informational(response: &Response) -> bool {
615+
// (100..200).contains(&response.status_code)
616+
// }
617+
618+
/// Check if [`Response`] status is within 200-299.
619+
fn is_success(response: &Response) -> bool {
620+
(200..300).contains(&response.status_code)
621+
}
622+
623+
// /// Check if [`Response`] status is within 300-399.
624+
// fn is_redirection(response: &Response) -> bool {
625+
// (300..400).contains(&response.status_code)
626+
// }
627+
628+
// /// Check if [`Response`] status is within 400-499.
629+
// fn is_client_error(response: &Response) -> bool {
630+
// (400..500).contains(&response.status_code)
631+
// }
632+
633+
// /// Check if [`Response`] status is within 500-599.
634+
// fn is_server_error(response: &Response) -> bool {
635+
// (500..600).contains(&response.status_code)
636+
// }
637+
638+
fn is_retryable(response: &Response) -> bool {
639+
RETRYABLE_ERROR_CODES.contains(&(response.status_code as u16))
624640
}
625641

626642
/// Sleeper trait that allows any async runtime to be used.

0 commit comments

Comments
 (0)