Skip to content

Commit f926914

Browse files
committed
Utilize reqwest::retry::Builder to implement HTTP retries
We here make use of `reqwest`'s `retry` functionlity that was introduced with its recent v0.12.23 release. As we can't fully replace the old 'application-level' behavior just yet, we configure it to only apply for error that arent INTERNAL_SERVER_ERROR, which is the status VSS uses to send error responses. We set some default values here, but note that these can always can be overridden by the user when using the `from_client` constructor.
1 parent 2364f09 commit f926914

File tree

4 files changed

+56
-25
lines changed

4 files changed

+56
-25
lines changed

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ lnurl-auth = ["dep:bitcoin", "dep:url", "dep:serde", "dep:serde_json", "reqwest/
1818

1919
[dependencies]
2020
prost = "0.11.6"
21-
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
21+
reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls"] }
2222
tokio = { version = "1", default-features = false, features = ["time"] }
2323
rand = "0.8.5"
2424
async-trait = "0.1.77"
@@ -33,7 +33,7 @@ chacha20-poly1305 = "0.1.2"
3333

3434
[target.'cfg(genproto)'.build-dependencies]
3535
prost-build = { version = "0.11.3" }
36-
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "blocking"] }
36+
reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls", "blocking"] }
3737

3838
[dev-dependencies]
3939
mockito = "0.28.0"

src/client.rs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::util::retry::{retry, RetryPolicy};
1515

1616
const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
1717
const DEFAULT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
18+
const DEFAULT_RETRIES: u32 = 2;
1819

1920
/// Thin-client to access a hosted instance of Versioned Storage Service (VSS).
2021
/// The provided [`VssClient`] API is minimalistic and is congruent to the VSS server-side API.
@@ -31,9 +32,9 @@ where
3132

3233
impl<R: RetryPolicy<E = VssError>> VssClient<R> {
3334
/// Constructs a [`VssClient`] using `base_url` as the VSS server endpoint.
34-
pub fn new(base_url: String, retry_policy: R) -> Self {
35-
let client = build_client();
36-
Self::from_client(base_url, client, retry_policy)
35+
pub fn new(base_url: String, retry_policy: R) -> Result<Self, VssError> {
36+
let client = build_client(&base_url)?;
37+
Ok(Self::from_client(base_url, client, retry_policy))
3738
}
3839

3940
/// Constructs a [`VssClient`] from a given [`reqwest::Client`], using `base_url` as the VSS server endpoint.
@@ -51,9 +52,9 @@ impl<R: RetryPolicy<E = VssError>> VssClient<R> {
5152
/// HTTP headers will be provided by the given `header_provider`.
5253
pub fn new_with_headers(
5354
base_url: String, retry_policy: R, header_provider: Arc<dyn VssHeaderProvider>,
54-
) -> Self {
55-
let client = build_client();
56-
Self { base_url, client, retry_policy, header_provider }
55+
) -> Result<Self, VssError> {
56+
let client = build_client(&base_url)?;
57+
Ok(Self { base_url, client, retry_policy, header_provider })
5758
}
5859

5960
/// Returns the underlying base URL.
@@ -164,11 +165,30 @@ impl<R: RetryPolicy<E = VssError>> VssClient<R> {
164165
}
165166
}
166167

167-
fn build_client() -> Client {
168-
Client::builder()
168+
fn build_client(base_url: &str) -> Result<Client, VssError> {
169+
let url = reqwest::Url::parse(base_url).map_err(|_| VssError::InvalidUrlError)?;
170+
let host_str = url.host_str().ok_or(VssError::InvalidUrlError)?.to_string();
171+
// TODO: Once the response `payload` is available in `classify_fn`, we should filter out any
172+
// error types for which retrying doesn't make sense (i.e., `NoSuchKeyError`,
173+
// `InvalidRequestError`,`ConflictError`).
174+
let retry = reqwest::retry::for_host(host_str)
175+
.max_retries_per_request(DEFAULT_RETRIES)
176+
.classify_fn(|req_rep| match req_rep.status() {
177+
// VSS uses INTERNAL_SERVER_ERROR when sending back error repsonses. These are
178+
// currently still covered by our `RetryPolicy`, so we tell `reqwest` not to retry them.
179+
Some(reqwest::StatusCode::INTERNAL_SERVER_ERROR) => req_rep.success(),
180+
Some(reqwest::StatusCode::BAD_REQUEST) => req_rep.success(),
181+
Some(reqwest::StatusCode::UNAUTHORIZED) => req_rep.success(),
182+
Some(reqwest::StatusCode::CONFLICT) => req_rep.success(),
183+
Some(reqwest::StatusCode::OK) => req_rep.success(),
184+
_ => req_rep.retryable(),
185+
});
186+
let client = Client::builder()
169187
.timeout(DEFAULT_TIMEOUT)
170188
.connect_timeout(DEFAULT_TIMEOUT)
171189
.read_timeout(DEFAULT_TIMEOUT)
190+
.retry(retry)
172191
.build()
173-
.unwrap()
192+
.unwrap();
193+
Ok(client)
174194
}

src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ pub enum VssError {
2828
/// There is an unknown error, it could be a client-side bug, unrecognized error-code, network error
2929
/// or something else.
3030
InternalError(String),
31+
32+
/// The provided URL is invalid.
33+
InvalidUrlError,
3134
}
3235

3336
impl VssError {
@@ -67,6 +70,9 @@ impl Display for VssError {
6770
VssError::InternalError(message) => {
6871
write!(f, "InternalError: {}", message)
6972
},
73+
VssError::InvalidUrlError => {
74+
write!(f, "The provided URL is invalid")
75+
},
7076
}
7177
}
7278
}

tests/tests.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ mod tests {
4848
.create();
4949

5050
// Create a new VssClient with the mock server URL.
51-
let client = VssClient::new(base_url, retry_policy());
51+
let client = VssClient::new(base_url, retry_policy()).unwrap();
5252

5353
let actual_result = client.get_object(&get_request).await.unwrap();
5454

@@ -85,7 +85,8 @@ mod tests {
8585
"headerkey".to_string(),
8686
"headervalue".to_string(),
8787
)])));
88-
let client = VssClient::new_with_headers(base_url, retry_policy(), header_provider);
88+
let client =
89+
VssClient::new_with_headers(base_url, retry_policy(), header_provider).unwrap();
8990

9091
let actual_result = client.get_object(&get_request).await.unwrap();
9192

@@ -123,7 +124,7 @@ mod tests {
123124
.create();
124125

125126
// Create a new VssClient with the mock server URL.
126-
let vss_client = VssClient::new(base_url, retry_policy());
127+
let vss_client = VssClient::new(base_url, retry_policy()).unwrap();
127128
let actual_result = vss_client.put_object(&request).await.unwrap();
128129

129130
let expected_result = &mock_response;
@@ -158,7 +159,7 @@ mod tests {
158159
.create();
159160

160161
// Create a new VssClient with the mock server URL.
161-
let vss_client = VssClient::new(base_url, retry_policy());
162+
let vss_client = VssClient::new(base_url, retry_policy()).unwrap();
162163
let actual_result = vss_client.delete_object(&request).await.unwrap();
163164

164165
let expected_result = &mock_response;
@@ -199,7 +200,7 @@ mod tests {
199200
.create();
200201

201202
// Create a new VssClient with the mock server URL.
202-
let client = VssClient::new(base_url, retry_policy());
203+
let client = VssClient::new(base_url, retry_policy()).unwrap();
203204

204205
let actual_result = client.list_key_versions(&request).await.unwrap();
205206

@@ -213,7 +214,7 @@ mod tests {
213214
#[tokio::test]
214215
async fn test_no_such_key_err_handling() {
215216
let base_url = mockito::server_url();
216-
let vss_client = VssClient::new(base_url, retry_policy());
217+
let vss_client = VssClient::new(base_url, retry_policy()).unwrap();
217218

218219
// NoSuchKeyError
219220
let error_response = ErrorResponse {
@@ -240,7 +241,7 @@ mod tests {
240241
#[tokio::test]
241242
async fn test_get_response_without_value() {
242243
let base_url = mockito::server_url();
243-
let vss_client = VssClient::new(base_url, retry_policy());
244+
let vss_client = VssClient::new(base_url, retry_policy()).unwrap();
244245

245246
// GetObjectResponse with None value
246247
let mock_response = GetObjectResponse { value: None, ..Default::default() };
@@ -261,7 +262,7 @@ mod tests {
261262
#[tokio::test]
262263
async fn test_invalid_request_err_handling() {
263264
let base_url = mockito::server_url();
264-
let vss_client = VssClient::new(base_url, retry_policy());
265+
let vss_client = VssClient::new(base_url, retry_policy()).unwrap();
265266

266267
// Invalid Request Error
267268
let error_response = ErrorResponse {
@@ -321,7 +322,7 @@ mod tests {
321322
#[tokio::test]
322323
async fn test_auth_err_handling() {
323324
let base_url = mockito::server_url();
324-
let vss_client = VssClient::new(base_url, retry_policy());
325+
let vss_client = VssClient::new(base_url, retry_policy()).unwrap();
325326

326327
// Invalid Request Error
327328
let error_response = ErrorResponse {
@@ -393,8 +394,12 @@ mod tests {
393394
async fn test_header_provider_error() {
394395
let get_request = GetObjectRequest { store_id: "store".to_string(), key: "k1".to_string() };
395396
let header_provider = Arc::new(FailingHeaderProvider {});
396-
let client =
397-
VssClient::new_with_headers("notused".to_string(), retry_policy(), header_provider);
397+
let client = VssClient::new_with_headers(
398+
"http://localhost/vss".to_string(),
399+
retry_policy(),
400+
header_provider,
401+
)
402+
.unwrap();
398403
let result = client.get_object(&get_request).await;
399404

400405
assert!(matches!(result, Err(VssError::AuthError { .. })));
@@ -403,7 +408,7 @@ mod tests {
403408
#[tokio::test]
404409
async fn test_conflict_err_handling() {
405410
let base_url = mockito::server_url();
406-
let vss_client = VssClient::new(base_url, retry_policy());
411+
let vss_client = VssClient::new(base_url, retry_policy()).unwrap();
407412

408413
// Conflict Error
409414
let error_response = ErrorResponse {
@@ -436,7 +441,7 @@ mod tests {
436441
#[tokio::test]
437442
async fn test_internal_server_err_handling() {
438443
let base_url = mockito::server_url();
439-
let vss_client = VssClient::new(base_url, retry_policy());
444+
let vss_client = VssClient::new(base_url, retry_policy()).unwrap();
440445

441446
// Internal Server Error
442447
let error_response = ErrorResponse {
@@ -496,7 +501,7 @@ mod tests {
496501
#[tokio::test]
497502
async fn test_internal_err_handling() {
498503
let base_url = mockito::server_url();
499-
let vss_client = VssClient::new(base_url, retry_policy());
504+
let vss_client = VssClient::new(base_url, retry_policy()).unwrap();
500505

501506
let error_response =
502507
ErrorResponse { error_code: 999, message: "UnknownException".to_string() };

0 commit comments

Comments
 (0)