Skip to content

Replacing reqwest crate with async_minreq crate #128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .clippy.toml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
msrv="1.63.0"
msrv="1.71.0"
9 changes: 3 additions & 6 deletions .github/workflows/cont_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
matrix:
rust:
- version: stable # STABLE
- version: 1.63.0 # MSRV
- version: 1.71.0 # MSRV
features:
- default
- blocking
Expand Down Expand Up @@ -52,16 +52,13 @@ jobs:
- name: Update toolchain
run: rustup update
- name: Pin dependencies for MSRV
if: matrix.rust.version == '1.63.0'
if: matrix.rust.version == '1.71.0'
run: |
cargo update -p reqwest --precise "0.12.4"
cargo update -p minreq --precise "2.13.2"
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
cargo update -p time --precise "0.3.20"
cargo update -p home --precise "0.5.5"
cargo update -p url --precise "2.5.0"
cargo update -p tokio --precise "1.38.1"
cargo update -p security-framework-sys --precise "2.11.1"
cargo update -p security-framework-sys --precise "2.14.0"
cargo update -p native-tls --precise "0.2.13"
cargo update -p ring --precise "0.17.12"
cargo update -p flate2 --precise "1.0.35"
Expand Down
14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ documentation = "https://docs.rs/esplora-client/"
description = "Bitcoin Esplora API client library. Supports plaintext, TLS and Onion servers. Blocking or async"
keywords = ["bitcoin", "esplora"]
readme = "README.md"
rust-version = "1.63.0"
rust-version = "1.71.0"

[lib]
name = "esplora_client"
Expand All @@ -22,7 +22,7 @@ bitcoin = { version = "0.32", features = ["serde", "std"], default-features = fa
hex = { version = "0.2", package = "hex-conservative" }
log = "^0.4"
minreq = { version = "2.11.0", features = ["json-using-serde"], optional = true }
reqwest = { version = "0.12", features = ["json"], default-features = false, optional = true }
async_minreq = { git = "https://github.com/psg-19/async-minreq", default-features = false, features = ["json-using-serde"], optional = true }

# default async runtime
tokio = { version = "1", features = ["time"], optional = true }
Expand All @@ -42,8 +42,8 @@ blocking-https-native = ["blocking", "minreq/https-native"]
blocking-https-bundled = ["blocking", "minreq/https-bundled"]

tokio = ["dep:tokio"]
async = ["reqwest", "reqwest/socks", "tokio?/time"]
async-https = ["async", "reqwest/default-tls"]
async-https-native = ["async", "reqwest/native-tls"]
async-https-rustls = ["async", "reqwest/rustls-tls"]
async-https-rustls-manual-roots = ["async", "reqwest/rustls-tls-manual-roots"]
async = ["async_minreq", "tokio?/time"]
async-https = ["async", "async_minreq/https"]
async-https-native = ["async", "async_minreq/https-native"]
async-https-rustls = ["async", "async_minreq/https-rustls"]
async-https-rustls-manual-roots = ["async"]
144 changes: 68 additions & 76 deletions src/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,86 +11,57 @@

//! Esplora by way of `reqwest` HTTP client.

use std::collections::HashMap;
use std::marker::PhantomData;
use std::str::FromStr;

use bitcoin::consensus::{deserialize, serialize, Decodable, Encodable};
use bitcoin::hashes::{sha256, Hash};
use bitcoin::hex::{DisplayHex, FromHex};
use bitcoin::Address;
use bitcoin::{
block::Header as BlockHeader, Block, BlockHash, MerkleBlock, Script, Transaction, Txid,
};

#[allow(unused_imports)]
use log::{debug, error, info, trace};

use reqwest::{header, Client, Response};
use std::collections::HashMap;
use std::marker::PhantomData;
use std::str::FromStr;

use crate::api::AddressStats;
use crate::{
BlockStatus, BlockSummary, Builder, Error, MerkleProof, OutputStatus, Tx, TxStatus,
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES,
BASE_BACKOFF_MILLIS, RETRYABLE_ERROR_CODES, VALID_HTTP_CODE
};
use async_minreq::{Method, Request, Response};
#[allow(unused_imports)]
use log::{debug, error, info, trace};

#[derive(Debug, Clone)]
pub struct AsyncClient<S = DefaultSleeper> {
/// The URL of the Esplora Server.
url: String,
/// The inner [`reqwest::Client`] to make HTTP requests.
client: Client,
/// Number of times to retry a request
/// Number of times to retry a request.
max_retries: usize,

/// Marker for the type of sleeper used
/// Default headers (applied to every request).
headers: HashMap<String, String>,
/// Marker for the sleeper.
marker: PhantomData<S>,
}

impl<S: Sleeper> AsyncClient<S> {
/// Build an async client from a builder
pub fn from_builder(builder: Builder) -> Result<Self, Error> {
let mut client_builder = Client::builder();

#[cfg(not(target_arch = "wasm32"))]
if let Some(proxy) = &builder.proxy {
client_builder = client_builder.proxy(reqwest::Proxy::all(proxy)?);
}

#[cfg(not(target_arch = "wasm32"))]
if let Some(timeout) = builder.timeout {
client_builder = client_builder.timeout(core::time::Duration::from_secs(timeout));
}

if !builder.headers.is_empty() {
let mut headers = header::HeaderMap::new();
for (k, v) in builder.headers {
let header_name = header::HeaderName::from_lowercase(k.to_lowercase().as_bytes())
.map_err(|_| Error::InvalidHttpHeaderName(k))?;
let header_value = header::HeaderValue::from_str(&v)
.map_err(|_| Error::InvalidHttpHeaderValue(v))?;
headers.insert(header_name, header_value);
}
client_builder = client_builder.default_headers(headers);
}

Ok(AsyncClient {
url: builder.base_url,
client: client_builder.build()?,
max_retries: builder.max_retries,
headers: builder.headers,
marker: PhantomData,
})
}

pub fn from_client(url: String, client: Client) -> Self {
pub fn from_client(url: String, headers: HashMap<String, String>) -> Self {
AsyncClient {
url,
client,
headers,
max_retries: crate::DEFAULT_MAX_RETRIES,
marker: PhantomData,
}
}

/// Make an HTTP GET request to given URL, deserializing to any `T` that
/// implement [`bitcoin::consensus::Decodable`].
///
Expand All @@ -106,14 +77,17 @@ impl<S: Sleeper> AsyncClient<S> {
let url = format!("{}{}", self.url, path);
let response = self.get_with_retry(&url).await?;

if !response.status().is_success() {
if response.status_code > VALID_HTTP_CODE {
return Err(Error::HttpResponse {
status: response.status().as_u16(),
message: response.text().await?,
status: response.status_code as u16,
message: match response.as_str() {
Ok(resp) => resp.to_string(),
Err(_) => return Err(Error::InvalidResponse),
},
});
}

Ok(deserialize::<T>(&response.bytes().await?)?)
Ok(deserialize::<T>(response.as_bytes())?)
}

/// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
Expand Down Expand Up @@ -146,14 +120,16 @@ impl<S: Sleeper> AsyncClient<S> {
let url = format!("{}{}", self.url, path);
let response = self.get_with_retry(&url).await?;

if !response.status().is_success() {
if response.status_code > VALID_HTTP_CODE {
return Err(Error::HttpResponse {
status: response.status().as_u16(),
message: response.text().await?,
status: response.status_code as u16,
message: match response.as_str() {
Ok(resp) => resp.to_string(),
Err(_) => return Err(Error::InvalidResponse),
},
});
}

response.json::<T>().await.map_err(Error::Reqwest)
response.json().map_err(Error::AsyncMinreq)
}

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

if !response.status().is_success() {
if response.status_code > VALID_HTTP_CODE {
return Err(Error::HttpResponse {
status: response.status().as_u16(),
message: response.text().await?,
status: response.status_code as u16,
message: match response.as_str() {
Ok(resp) => resp.to_string(),
Err(_) => return Err(Error::InvalidResponse),
},
});
}

let hex_str = response.text().await?;
let hex_str = match response.as_str() {
Ok(resp) => resp.to_string(),
Err(_) => return Err(Error::InvalidResponse),
};
Ok(deserialize(&Vec::from_hex(&hex_str)?)?)
}

Expand Down Expand Up @@ -225,14 +206,19 @@ impl<S: Sleeper> AsyncClient<S> {
let url = format!("{}{}", self.url, path);
let response = self.get_with_retry(&url).await?;

if !response.status().is_success() {
if response.status_code > VALID_HTTP_CODE {
return Err(Error::HttpResponse {
status: response.status().as_u16(),
message: response.text().await?,
status: response.status_code as u16,
message: match response.as_str() {
Ok(resp) => resp.to_string(),
Err(_) => return Err(Error::InvalidResponse),
},
});
}

Ok(response.text().await?)
Ok(match response.as_str() {
Ok(resp) => resp.to_string(),
Err(_) => return Err(Error::InvalidResponse),
})
}

/// Make an HTTP GET request to given URL, deserializing to `Option<T>`.
Expand Down Expand Up @@ -263,15 +249,21 @@ impl<S: Sleeper> AsyncClient<S> {
let url = format!("{}{}", self.url, path);
let body = serialize::<T>(&body).to_lower_hex_string();

let response = self.client.post(url).body(body).send().await?;
let mut request = Request::new(Method::Post, &url).with_body(body);
for (key, value) in &self.headers {
request = request.with_header(key, value);
}

if !response.status().is_success() {
let response = request.send().await.map_err(Error::AsyncMinreq)?;
if response.status_code > VALID_HTTP_CODE {
return Err(Error::HttpResponse {
status: response.status().as_u16(),
message: response.text().await?,
status: response.status_code as u16,
message: match response.as_str() {
Ok(resp) => resp.to_string(),
Err(_) => return Err(Error::InvalidResponse),
},
});
}

Ok(())
}

Expand Down Expand Up @@ -454,20 +446,20 @@ impl<S: Sleeper> AsyncClient<S> {
&self.url
}

/// Get the underlying [`Client`].
pub fn client(&self) -> &Client {
&self.client
}

/// Sends a GET request to the given `url`, retrying failed attempts
/// for retryable error codes until max retries hit.
async fn get_with_retry(&self, url: &str) -> Result<Response, Error> {
let mut delay = BASE_BACKOFF_MILLIS;
let mut attempts = 0;

loop {
match self.client.get(url).send().await? {
resp if attempts < self.max_retries && is_status_retryable(resp.status()) => {
let mut request = Request::new(Method::Get, url);
for (key, value) in &self.headers {
request = request.with_header(key, value);
}

match request.send().await? {
resp if attempts < self.max_retries && is_status_retryable(resp.status_code) => {
S::sleep(delay).await;
attempts += 1;
delay *= 2;
Expand All @@ -478,8 +470,8 @@ impl<S: Sleeper> AsyncClient<S> {
}
}

fn is_status_retryable(status: reqwest::StatusCode) -> bool {
RETRYABLE_ERROR_CODES.contains(&status.as_u16())
fn is_status_retryable(status: i32) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If async_minreq project is open to it would be nice to have a similar StatusCode enum in that project to use here and also instead of your 299 value above.

RETRYABLE_ERROR_CODES.contains(&(status as u16))
}

pub trait Sleeper: 'static {
Expand Down
10 changes: 6 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ const BASE_BACKOFF_MILLIS: Duration = Duration::from_millis(256);
/// Default max retries.
const DEFAULT_MAX_RETRIES: usize = 6;

/// Valid HTTP code
const VALID_HTTP_CODE: i32 = 299;

/// Get a fee value in sats/vbytes from the estimates
/// that matches the confirmation target set as parameter.
///
Expand Down Expand Up @@ -200,9 +203,9 @@ pub enum Error {
/// Error during `minreq` HTTP request
#[cfg(feature = "blocking")]
Minreq(::minreq::Error),
/// Error during reqwest HTTP request
/// Error during async_minreq HTTP request
#[cfg(feature = "async")]
Reqwest(::reqwest::Error),
AsyncMinreq(async_minreq::Error),
/// HTTP response error
HttpResponse { status: u16, message: String },
/// Invalid number returned
Expand Down Expand Up @@ -247,12 +250,11 @@ macro_rules! impl_error {
}
};
}

impl std::error::Error for Error {}
#[cfg(feature = "blocking")]
impl_error!(::minreq::Error, Minreq, Error);
#[cfg(feature = "async")]
impl_error!(::reqwest::Error, Reqwest, Error);
impl_error!(::async_minreq::Error, AsyncMinreq, Error);
impl_error!(std::num::ParseIntError, Parsing, Error);
impl_error!(bitcoin::consensus::encode::Error, BitcoinEncoding, Error);
impl_error!(bitcoin::hex::HexToArrayError, HexToArray, Error);
Expand Down