Skip to content

Commit 6a794e6

Browse files
feat: add user agent header to HTTP requests (#473)
* chore: add coverage artifacts to gitignore * test(redisctl): add unit tests for async_utils pure functions Add comprehensive unit tests for async operation utilities: - Task state detection (completed, failed, cancelled, in-progress) - Task state formatting with visual indicators - Task details parsing from various API response formats - Error handling for nested and alternative field names Coverage: 22 new tests for critical polling/async logic Related to #462 * feat: add user agent header to HTTP requests Add configurable User-Agent header support to both Cloud and Enterprise API clients for request tracking purposes. - Add user_agent field to CloudClientBuilder with default redis-cloud/{version} - Add user_agent field to EnterpriseClientBuilder with default redis-enterprise/{version} - Configure redisctl CLI to use redisctl/{version} as user agent - Set User-Agent via default_headers() on reqwest client builder
1 parent 068d893 commit 6a794e6

File tree

3 files changed

+55
-2
lines changed

3 files changed

+55
-2
lines changed

crates/redis-cloud/src/client.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
99
use crate::{CloudError as RestError, Result};
1010
use reqwest::Client;
11+
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
1112
use serde::Serialize;
1213
use std::sync::Arc;
1314
use tracing::{debug, instrument, trace};
1415

16+
/// Default user agent for the Redis Cloud client
17+
const DEFAULT_USER_AGENT: &str = concat!("redis-cloud/", env!("CARGO_PKG_VERSION"));
18+
1519
/// Builder for constructing a CloudClient with custom configuration
1620
///
1721
/// Provides a fluent interface for configuring API credentials, base URL, timeouts,
@@ -43,6 +47,7 @@ pub struct CloudClientBuilder {
4347
api_secret: Option<String>,
4448
base_url: String,
4549
timeout: std::time::Duration,
50+
user_agent: String,
4651
}
4752

4853
impl Default for CloudClientBuilder {
@@ -52,6 +57,7 @@ impl Default for CloudClientBuilder {
5257
api_secret: None,
5358
base_url: "https://api.redislabs.com/v1".to_string(),
5459
timeout: std::time::Duration::from_secs(30),
60+
user_agent: DEFAULT_USER_AGENT.to_string(),
5561
}
5662
}
5763
}
@@ -86,6 +92,16 @@ impl CloudClientBuilder {
8692
self
8793
}
8894

95+
/// Set the user agent string for HTTP requests
96+
///
97+
/// The default user agent is `redis-cloud/{version}`.
98+
/// This can be overridden to identify specific clients, for example:
99+
/// `redisctl/1.2.3` or `my-app/1.0.0`.
100+
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
101+
self.user_agent = user_agent.into();
102+
self
103+
}
104+
89105
/// Build the client
90106
pub fn build(self) -> Result<CloudClient> {
91107
let api_key = self
@@ -95,8 +111,16 @@ impl CloudClientBuilder {
95111
.api_secret
96112
.ok_or_else(|| RestError::ConnectionError("API secret is required".to_string()))?;
97113

114+
let mut default_headers = HeaderMap::new();
115+
default_headers.insert(
116+
USER_AGENT,
117+
HeaderValue::from_str(&self.user_agent)
118+
.map_err(|e| RestError::ConnectionError(format!("Invalid user agent: {}", e)))?,
119+
);
120+
98121
let client = Client::builder()
99122
.timeout(self.timeout)
123+
.default_headers(default_headers)
100124
.build()
101125
.map_err(|e| RestError::ConnectionError(e.to_string()))?;
102126

crates/redis-enterprise/src/client.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
//! REST API client implementation
22
33
use crate::error::{RestError, Result};
4+
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
45
use reqwest::{Client, Response};
56
use serde::{Serialize, de::DeserializeOwned};
67
use std::sync::Arc;
78
use std::time::Duration;
89
use tracing::{debug, trace};
910

11+
/// Default user agent for the Redis Enterprise client
12+
const DEFAULT_USER_AGENT: &str = concat!("redis-enterprise/", env!("CARGO_PKG_VERSION"));
13+
1014
// Legacy alias for backwards compatibility during migration
1115
pub type RestConfig = EnterpriseClientBuilder;
1216

@@ -18,6 +22,7 @@ pub struct EnterpriseClientBuilder {
1822
password: Option<String>,
1923
timeout: Duration,
2024
insecure: bool,
25+
user_agent: String,
2126
}
2227

2328
impl Default for EnterpriseClientBuilder {
@@ -28,6 +33,7 @@ impl Default for EnterpriseClientBuilder {
2833
password: None,
2934
timeout: Duration::from_secs(30),
3035
insecure: false,
36+
user_agent: DEFAULT_USER_AGENT.to_string(),
3137
}
3238
}
3339
}
@@ -68,14 +74,32 @@ impl EnterpriseClientBuilder {
6874
self
6975
}
7076

77+
/// Set the user agent string for HTTP requests
78+
///
79+
/// The default user agent is `redis-enterprise/{version}`.
80+
/// This can be overridden to identify specific clients, for example:
81+
/// `redisctl/1.2.3` or `my-app/1.0.0`.
82+
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
83+
self.user_agent = user_agent.into();
84+
self
85+
}
86+
7187
/// Build the client
7288
pub fn build(self) -> Result<EnterpriseClient> {
7389
let username = self.username.unwrap_or_default();
7490
let password = self.password.unwrap_or_default();
7591

92+
let mut default_headers = HeaderMap::new();
93+
default_headers.insert(
94+
USER_AGENT,
95+
HeaderValue::from_str(&self.user_agent)
96+
.map_err(|e| RestError::ConnectionError(format!("Invalid user agent: {}", e)))?,
97+
);
98+
7699
let client_builder = Client::builder()
77100
.timeout(self.timeout)
78-
.danger_accept_invalid_certs(self.insecure);
101+
.danger_accept_invalid_certs(self.insecure)
102+
.default_headers(default_headers);
79103

80104
let client = client_builder
81105
.build()

crates/redisctl/src/connection.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ use anyhow::Context;
55
use redisctl_config::{Config, DeploymentType};
66
use tracing::{debug, info, trace};
77

8+
/// User agent string for redisctl HTTP requests
9+
const REDISCTL_USER_AGENT: &str = concat!("redisctl/", env!("CARGO_PKG_VERSION"));
10+
811
/// Connection manager for creating authenticated clients
912
#[allow(dead_code)] // Used by binary target
1013
#[derive(Clone)]
@@ -161,6 +164,7 @@ impl ConnectionManager {
161164
.api_key(&final_api_key)
162165
.api_secret(&final_api_secret)
163166
.base_url(&final_api_url)
167+
.user_agent(REDISCTL_USER_AGENT)
164168
.build()
165169
.context("Failed to create Redis Cloud client")?;
166170

@@ -306,7 +310,8 @@ impl ConnectionManager {
306310
// Build the Enterprise client
307311
let mut builder = redis_enterprise::EnterpriseClient::builder()
308312
.base_url(&final_url)
309-
.username(&final_username);
313+
.username(&final_username)
314+
.user_agent(REDISCTL_USER_AGENT);
310315

311316
// Add password if provided
312317
if let Some(ref password) = final_password {

0 commit comments

Comments
 (0)