Skip to content

Commit 59c2218

Browse files
committed
Switch from reqwest to bitreq HTTP client
Replace the `reqwest` dependency with `bitreq`, a lighter HTTP(s) client that reduces code bloat and dependency overhead. Key changes: - Use `bitreq::Client` for connection pooling and reuse - Update all HTTP request handling to use bitreq's API - Remove `reqwest::header::HeaderMap` in favor of `HashMap<String, String>` - Simplify `LnurlAuthToJwtProvider::new()` to no longer return a `Result` - Use `serde_json::from_slice()` directly for JSON parsing - Build script uses bitreq's blocking `send()` method Co-Authored-By: HAL 9000 Signed-off-by: Elias Rohrer <dev@tnull.de>
1 parent 61c1626 commit 59c2218

File tree

9 files changed

+103
-118
lines changed

9 files changed

+103
-118
lines changed

Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,28 @@ build = "build.rs"
1515

1616
[features]
1717
default = ["lnurl-auth", "sigs-auth"]
18-
lnurl-auth = ["dep:bitcoin", "dep:url", "dep:serde", "dep:serde_json", "reqwest/json"]
18+
lnurl-auth = ["dep:bitcoin", "dep:url", "dep:serde", "dep:serde_json"]
1919
sigs-auth = ["dep:bitcoin"]
2020

2121
[dependencies]
2222
prost = "0.11.6"
23-
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
23+
bitreq = { version = "0.3", default-features = false, features = ["async-https"] }
2424
tokio = { version = "1", default-features = false, features = ["time"] }
2525
rand = "0.8.5"
2626
async-trait = "0.1.77"
2727
bitcoin = { version = "0.32.2", default-features = false, features = ["std", "rand-std"], optional = true }
28-
url = { version = "2.5.0", default-features = false, optional = true }
29-
base64 = { version = "0.22", default-features = false}
28+
url = { version = "2.5.0", default-features = false, optional = true, features = ["std"] }
29+
base64 = { version = "0.22", default-features = false, features = ["std"]}
3030
serde = { version = "1.0.196", default-features = false, features = ["serde_derive"], optional = true }
31-
serde_json = { version = "1.0.113", default-features = false, optional = true }
31+
serde_json = { version = "1.0.113", default-features = false, features = ["std"], optional = true }
3232

3333
bitcoin_hashes = "0.14.0"
3434
chacha20-poly1305 = "0.1.2"
3535
log = { version = "0.4.29", default-features = false, features = ["std"]}
3636

3737
[target.'cfg(genproto)'.build-dependencies]
3838
prost-build = { version = "0.11.3" }
39-
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "blocking"] }
39+
bitreq = { version = "0.3", default-features = false, features = ["std", "https"] }
4040

4141
[dev-dependencies]
4242
mockito = "0.28.0"

build.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#[cfg(genproto)]
22
extern crate prost_build;
33
#[cfg(genproto)]
4+
use std::io::Write;
5+
#[cfg(genproto)]
46
use std::{env, fs, fs::File, path::Path};
57

68
/// To generate updated proto objects:
@@ -25,9 +27,9 @@ fn generate_protos() {
2527

2628
#[cfg(genproto)]
2729
fn download_file(url: &str, save_to: &str) -> Result<(), Box<dyn std::error::Error>> {
28-
let mut response = reqwest::blocking::get(url)?;
30+
let response = bitreq::get(url).send()?;
2931
fs::create_dir_all(Path::new(save_to).parent().unwrap())?;
3032
let mut out_file = File::create(save_to)?;
31-
response.copy_to(&mut out_file)?;
33+
out_file.write_all(&response.into_bytes())?;
3234
Ok(())
3335
}

src/client.rs

Lines changed: 45 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1+
use bitreq::Client;
12
use prost::Message;
2-
use reqwest::header::CONTENT_TYPE;
3-
use reqwest::Client;
43
use std::collections::HashMap;
54
use std::default::Default;
65
use std::sync::Arc;
76

87
use log::trace;
98

109
use crate::error::VssError;
11-
use crate::headers::{get_headermap, FixedHeaders, VssHeaderProvider};
10+
use crate::headers::{FixedHeaders, VssHeaderProvider};
1211
use crate::types::{
1312
DeleteObjectRequest, DeleteObjectResponse, GetObjectRequest, GetObjectResponse,
1413
ListKeyVersionsRequest, ListKeyVersionsResponse, PutObjectRequest, PutObjectResponse,
@@ -17,7 +16,10 @@ use crate::util::retry::{retry, RetryPolicy};
1716
use crate::util::KeyValueVecKeyPrinter;
1817

1918
const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
20-
const DEFAULT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
19+
const CONTENT_TYPE: &str = "content-type";
20+
const DEFAULT_TIMEOUT_SECS: u64 = 10;
21+
const MAX_RESPONSE_BODY_SIZE: usize = 1024 * 1024 * 1024; // 1GB
22+
const DEFAULT_CLIENT_CAPACITY: usize = 10;
2123

2224
/// Thin-client to access a hosted instance of Versioned Storage Service (VSS).
2325
/// The provided [`VssClient`] API is minimalistic and is congruent to the VSS server-side API.
@@ -35,11 +37,11 @@ where
3537
impl<R: RetryPolicy<E = VssError>> VssClient<R> {
3638
/// Constructs a [`VssClient`] using `base_url` as the VSS server endpoint.
3739
pub fn new(base_url: String, retry_policy: R) -> Self {
38-
let client = build_client();
40+
let client = Client::new(DEFAULT_CLIENT_CAPACITY);
3941
Self::from_client(base_url, client, retry_policy)
4042
}
4143

42-
/// Constructs a [`VssClient`] from a given [`reqwest::Client`], using `base_url` as the VSS server endpoint.
44+
/// Constructs a [`VssClient`] from a given [`bitreq::Client`], using `base_url` as the VSS server endpoint.
4345
pub fn from_client(base_url: String, client: Client, retry_policy: R) -> Self {
4446
Self {
4547
base_url,
@@ -49,7 +51,7 @@ impl<R: RetryPolicy<E = VssError>> VssClient<R> {
4951
}
5052
}
5153

52-
/// Constructs a [`VssClient`] from a given [`reqwest::Client`], using `base_url` as the VSS server endpoint.
54+
/// Constructs a [`VssClient`] from a given [`bitreq::Client`], using `base_url` as the VSS server endpoint.
5355
///
5456
/// HTTP headers will be provided by the given `header_provider`.
5557
pub fn from_client_and_headers(
@@ -65,7 +67,7 @@ impl<R: RetryPolicy<E = VssError>> VssClient<R> {
6567
pub fn new_with_headers(
6668
base_url: String, retry_policy: R, header_provider: Arc<dyn VssHeaderProvider>,
6769
) -> Self {
68-
let client = build_client();
70+
let client = Client::new(DEFAULT_CLIENT_CAPACITY);
6971
Self { base_url, client, retry_policy, header_provider }
7072
}
7173

@@ -85,15 +87,17 @@ impl<R: RetryPolicy<E = VssError>> VssClient<R> {
8587
let res = retry(
8688
|| async {
8789
let url = format!("{}/getObject", self.base_url);
88-
self.post_request(request, &url).await.and_then(|response: GetObjectResponse| {
89-
if response.value.is_none() {
90-
Err(VssError::InternalServerError(
91-
"VSS Server API Violation, expected value in GetObjectResponse but found none".to_string(),
92-
))
93-
} else {
94-
Ok(response)
95-
}
96-
})
90+
self.post_request(request, &url, true).await.and_then(
91+
|response: GetObjectResponse| {
92+
if response.value.is_none() {
93+
Err(VssError::InternalServerError(
94+
"VSS Server API Violation, expected value in GetObjectResponse but found none".to_string(),
95+
))
96+
} else {
97+
Ok(response)
98+
}
99+
},
100+
)
97101
},
98102
&self.retry_policy,
99103
)
@@ -121,7 +125,7 @@ impl<R: RetryPolicy<E = VssError>> VssClient<R> {
121125
let res = retry(
122126
|| async {
123127
let url = format!("{}/putObjects", self.base_url);
124-
self.post_request(request, &url).await
128+
self.post_request(request, &url, false).await
125129
},
126130
&self.retry_policy,
127131
)
@@ -147,7 +151,7 @@ impl<R: RetryPolicy<E = VssError>> VssClient<R> {
147151
let res = retry(
148152
|| async {
149153
let url = format!("{}/deleteObject", self.base_url);
150-
self.post_request(request, &url).await
154+
self.post_request(request, &url, true).await
151155
},
152156
&self.retry_policy,
153157
)
@@ -175,7 +179,7 @@ impl<R: RetryPolicy<E = VssError>> VssClient<R> {
175179
let res = retry(
176180
|| async {
177181
let url = format!("{}/listKeyVersions", self.base_url);
178-
self.post_request(request, &url).await
182+
self.post_request(request, &url, true).await
179183
},
180184
&self.retry_policy,
181185
)
@@ -187,40 +191,36 @@ impl<R: RetryPolicy<E = VssError>> VssClient<R> {
187191
}
188192

189193
async fn post_request<Rq: Message, Rs: Message + Default>(
190-
&self, request: &Rq, url: &str,
194+
&self, request: &Rq, url: &str, enable_pipelining: bool,
191195
) -> Result<Rs, VssError> {
192196
let request_body = request.encode_to_vec();
193-
let headermap = self
197+
let headers = self
194198
.header_provider
195199
.get_headers(&request_body)
196200
.await
197-
.and_then(|h| get_headermap(&h))
198201
.map_err(|e| VssError::AuthError(e.to_string()))?;
199-
let response_raw = self
200-
.client
201-
.post(url)
202-
.header(CONTENT_TYPE, APPLICATION_OCTET_STREAM)
203-
.headers(headermap)
204-
.body(request_body)
205-
.send()
206-
.await?;
207-
let status = response_raw.status();
208-
let payload = response_raw.bytes().await?;
209-
210-
if status.is_success() {
202+
203+
let mut http_request = bitreq::post(url)
204+
.with_header(CONTENT_TYPE, APPLICATION_OCTET_STREAM)
205+
.with_headers(headers)
206+
.with_body(request_body)
207+
.with_timeout(DEFAULT_TIMEOUT_SECS)
208+
.with_max_body_size(Some(MAX_RESPONSE_BODY_SIZE));
209+
210+
if enable_pipelining {
211+
http_request = http_request.with_pipelining();
212+
}
213+
214+
let response = self.client.send_async(http_request).await?;
215+
216+
let status_code = response.status_code;
217+
let payload = response.into_bytes();
218+
219+
if (200..300).contains(&status_code) {
211220
let response = Rs::decode(&payload[..])?;
212221
Ok(response)
213222
} else {
214-
Err(VssError::new(status, payload))
223+
Err(VssError::new(status_code, payload))
215224
}
216225
}
217226
}
218-
219-
fn build_client() -> Client {
220-
Client::builder()
221-
.timeout(DEFAULT_TIMEOUT)
222-
.connect_timeout(DEFAULT_TIMEOUT)
223-
.read_timeout(DEFAULT_TIMEOUT)
224-
.build()
225-
.unwrap()
226-
}

src/error.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use crate::types::{ErrorCode, ErrorResponse};
2-
use prost::bytes::Bytes;
32
use prost::{DecodeError, Message};
4-
use reqwest::StatusCode;
53
use std::error::Error;
64
use std::fmt::{Display, Formatter};
75

@@ -32,13 +30,13 @@ pub enum VssError {
3230

3331
impl VssError {
3432
/// Create new instance of `VssError`
35-
pub fn new(status: StatusCode, payload: Bytes) -> VssError {
33+
pub fn new(status_code: i32, payload: Vec<u8>) -> VssError {
3634
match ErrorResponse::decode(&payload[..]) {
3735
Ok(error_response) => VssError::from(error_response),
3836
Err(e) => {
3937
let message = format!(
4038
"Unable to decode ErrorResponse from server, HttpStatusCode: {}, DecodeErr: {}",
41-
status, e
39+
status_code, e
4240
);
4341
VssError::InternalError(message)
4442
},
@@ -99,8 +97,8 @@ impl From<DecodeError> for VssError {
9997
}
10098
}
10199

102-
impl From<reqwest::Error> for VssError {
103-
fn from(err: reqwest::Error) -> Self {
100+
impl From<bitreq::Error> for VssError {
101+
fn from(err: bitreq::Error) -> Self {
104102
VssError::InternalError(err.to_string())
105103
}
106104
}

src/headers/lnurl_auth_jwt.rs

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::headers::{get_headermap, VssHeaderProvider, VssHeaderProviderError};
1+
use crate::headers::{VssHeaderProvider, VssHeaderProviderError};
22
use async_trait::async_trait;
33
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
44
use base64::Engine;
@@ -26,6 +26,8 @@ const SIG_QUERY_PARAM: &str = "sig";
2626
const KEY_QUERY_PARAM: &str = "key";
2727
// The authorization header name.
2828
const AUTHORIZATION: &str = "Authorization";
29+
// The maximum body size we allow for requests.
30+
const MAX_RESPONSE_BODY_SIZE: usize = 16 * 1024 * 1024; // 16 KB
2931

3032
#[derive(Debug, Clone)]
3133
struct JwtToken {
@@ -45,13 +47,14 @@ impl JwtToken {
4547
}
4648
}
4749

50+
const DEFAULT_TIMEOUT_SECS: u64 = 10;
51+
4852
/// Provides a JWT token based on LNURL Auth.
4953
pub struct LnurlAuthToJwtProvider {
5054
engine: Secp256k1<SignOnly>,
5155
parent_key: Xpriv,
5256
url: String,
5357
default_headers: HashMap<String, String>,
54-
client: reqwest::Client,
5558
cached_jwt_token: RwLock<Option<JwtToken>>,
5659
}
5760

@@ -70,47 +73,46 @@ impl LnurlAuthToJwtProvider {
7073
/// with the JWT authorization header for VSS requests.
7174
pub fn new(
7275
parent_key: Xpriv, url: String, default_headers: HashMap<String, String>,
73-
) -> Result<LnurlAuthToJwtProvider, VssHeaderProviderError> {
76+
) -> LnurlAuthToJwtProvider {
7477
let engine = Secp256k1::signing_only();
75-
let default_headermap = get_headermap(&default_headers)?;
76-
let client = reqwest::Client::builder()
77-
.default_headers(default_headermap)
78-
.build()
79-
.map_err(VssHeaderProviderError::from)?;
8078

81-
Ok(LnurlAuthToJwtProvider {
79+
LnurlAuthToJwtProvider {
8280
engine,
8381
parent_key,
8482
url,
8583
default_headers,
86-
client,
8784
cached_jwt_token: RwLock::new(None),
88-
})
85+
}
8986
}
9087

9188
async fn fetch_jwt_token(&self) -> Result<JwtToken, VssHeaderProviderError> {
9289
// Fetch the LNURL.
93-
let lnurl_str = self
94-
.client
95-
.get(&self.url)
96-
.send()
97-
.await
98-
.map_err(VssHeaderProviderError::from)?
99-
.text()
100-
.await
101-
.map_err(VssHeaderProviderError::from)?;
90+
let lnurl_request = bitreq::get(&self.url)
91+
.with_headers(self.default_headers.clone())
92+
.with_timeout(DEFAULT_TIMEOUT_SECS)
93+
.with_max_body_size(Some(MAX_RESPONSE_BODY_SIZE));
94+
let lnurl_response =
95+
lnurl_request.send_async().await.map_err(VssHeaderProviderError::from)?;
96+
let lnurl_str = String::from_utf8(lnurl_response.into_bytes()).map_err(|e| {
97+
VssHeaderProviderError::InvalidData {
98+
error: format!("LNURL response is not valid UTF-8: {}", e),
99+
}
100+
})?;
102101

103102
// Sign the LNURL and perform the request.
104103
let signed_lnurl = sign_lnurl(&self.engine, &self.parent_key, &lnurl_str)?;
105-
let lnurl_auth_response: LnurlAuthResponse = self
106-
.client
107-
.get(&signed_lnurl)
108-
.send()
109-
.await
110-
.map_err(VssHeaderProviderError::from)?
111-
.json()
112-
.await
113-
.map_err(VssHeaderProviderError::from)?;
104+
let auth_request = bitreq::get(&signed_lnurl)
105+
.with_headers(self.default_headers.clone())
106+
.with_timeout(DEFAULT_TIMEOUT_SECS)
107+
.with_max_body_size(Some(MAX_RESPONSE_BODY_SIZE));
108+
let auth_response =
109+
auth_request.send_async().await.map_err(VssHeaderProviderError::from)?;
110+
let lnurl_auth_response: LnurlAuthResponse =
111+
serde_json::from_slice(&auth_response.into_bytes()).map_err(|e| {
112+
VssHeaderProviderError::InvalidData {
113+
error: format!("Failed to parse LNURL Auth response as JSON: {}", e),
114+
}
115+
})?;
114116

115117
let untrusted_token = match lnurl_auth_response {
116118
LnurlAuthResponse { token: Some(token), .. } => token,
@@ -256,8 +258,8 @@ impl From<bitcoin::bip32::Error> for VssHeaderProviderError {
256258
}
257259
}
258260

259-
impl From<reqwest::Error> for VssHeaderProviderError {
260-
fn from(e: reqwest::Error) -> VssHeaderProviderError {
261+
impl From<bitreq::Error> for VssHeaderProviderError {
262+
fn from(e: bitreq::Error) -> VssHeaderProviderError {
261263
VssHeaderProviderError::RequestError { error: e.to_string() }
262264
}
263265
}

0 commit comments

Comments
 (0)