A high-performance, Tower-backed HTTP API client for Rust.
- Tower Stack: Leverages the Tower ecosystem for middleware (load balancing, retrying, rate limiting, etc.).
- Audit Logging: Built-in audit layer that logs requests as
curlcommands and pretty-prints JSON responses. - Custom & S3 Auditors: First-class support for storing audit logs in S3/Garage object storage or custom audit backends.
- Concurrency Control: Optional semaphore-based concurrency limiting.
- Connection Pooling: Configurable idle connection limits per host.
- Cookie Support: Automatic cookie management and session jar clearing.
- Retry Logic: Automatic retries for idempotent requests on transient errors and 5xx status codes.
- Response Streaming: Stream large response bodies with audit mute support.
Add api-client to your Cargo.toml:
[dependencies]
api-client = { git = "https://github.com/bixority/api-client" }To enable audit logging functionality and S3/Garage object storage integration, enable the audit feature:
[dependencies]
api-client = { git = "https://github.com/bixority/api-client", features = ["audit"] }
object-storage-client = { version = "0.1", registry = "bixority-codeberg" }When using the audit feature with ObjectStorageAuditor, connection settings and S3 credentials are automatically
loaded from environment variables:
API_CLIENT_AUDIT_PATH: Root path for audit log storage (loaded intoAuditConfig).S3_ENDPOINT(orAWS_ENDPOINT_URL_S3,AWS_ENDPOINT,AWS_ENDPOINT_URL): Custom endpoint URL for S3 or S3-compatible storage (e.g.,http://localhost:3900for Garage).S3_ACCESS_KEY_ID(orAWS_ACCESS_KEY_ID): Access key ID / username.S3_SECRET_ACCESS_KEY(orAWS_SECRET_ACCESS_KEY): Secret access key / password.S3_REGION(orAWS_REGION): Storage region (defaults tous-east-1if not specified).S3_ALLOW_HTTP: Set totrueto allow plain HTTP connections for local development (automatically permitted if endpoint begins withhttp://).
export S3_ENDPOINT="http://localhost:9000"
export S3_ACCESS_KEY_ID="rootroot"
export S3_SECRET_ACCESS_KEY="rootrootrootroot"
export S3_REGION="us-east-1"
export S3_ALLOW_HTTP=trueBelow is a complete usage example demonstrating how to configure environment variables, initialize APIClient with
ObjectStorageAuditor, and execute audited HTTP requests:
use api_client::audit::ObjectStorageAuditor;
use api_client::{APIClient, AuditConfig, Headers, Method};
use object_storage_client::ObjectStorageClient;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Setup environment variables (typically done via shell / .env file)
// std::env::set_var("S3_ENDPOINT", "http://localhost:3900");
// std::env::set_var("S3_ACCESS_KEY_ID", "rootroot");
// std::env::set_var("S3_SECRET_ACCESS_KEY", "rootrootrootroot");
// std::env::set_var("S3_REGION", "us-east-1");
// std::env::set_var("S3_ALLOW_HTTP", "true");
// 2. Initialize ObjectStorageClient and ObjectStorageAuditor
let storage_client = ObjectStorageClient::new();
let auditor = ObjectStorageAuditor::new(storage_client);
// 3. Build APIClient with the auditor and custom options
let client = APIClient::new("https://httpbin.org".to_string())
.timeout_secs(10)
.max_concurrent(Some(10))
.pool_max_idle_per_host(5)
.with_auditor(Arc::new(auditor))
.build()?;
// 4. Construct request headers
let headers = Headers::new()
.content_type("application/json")
.authorization_bearer("your-token");
// 5. Configure audit entry name for this request
let audit_config = AuditConfig::new("user-login");
// 6. Execute the request
let response = client
.request(
"/post",
Method::Post,
headers,
Some(b"{\"username\": \"alice\"}".to_vec()),
None, // query params
Some(audit_config),
)
.await?;
if response.status().is_success() {
let text = response.text().await?;
println!("Response: {}", text);
}
Ok(())
}Note: If the
auditfeature is disabled,APIClient::requestaccepts 5 parameters (uri,method,headers,body,query_params) without the trailingOption<AuditConfig>.
Note: Audit logging requires the
auditfeature flag to be enabled inCargo.toml.
The client includes an auditing layer that captures:
- Request: Reconstructs the outgoing request as an executable
curlcommand. - Response: Logs response status and pretty-prints JSON bodies (or raw text).
If no custom auditor is passed via .with_auditor(...), the client automatically logs audit output through the
tracing crate at INFO level.
ObjectStorageAuditor uploads audit files to any S3-compatible object storage. It writes each audit object under the
key provided by AuditConfig / AuditMetadata without adding any prefix of its own. The root path comes from
AuditConfig (set via API_CLIENT_AUDIT_PATH or AuditConfig::with_path(...)).
use api_client::APIClient;
use api_client::audit::ObjectStorageAuditor;
use object_storage_client::ObjectStorageClient;
use std::sync::Arc;
let storage_client = ObjectStorageClient::new();
let auditor = ObjectStorageAuditor::new(storage_client);
let client = APIClient::new("https://api.example.com")
.with_auditor(Arc::new(auditor))
.build()?;When an audit name is provided in AuditConfig, files are uploaded using the following structured path layout:
[{root_path}/]YYYY/MM/DD/{audit_name}/{uri_path}/{METHOD}_{YYMMDD_HHMMSS}_{request_id}_{request|response}.txt
You can implement the Auditor trait to route audit entries to any backend (e.g., custom database, message queue, or
stdout console). See examples/audit.rs for a complete working example.
use api_client::{APIClient, APIClientError, Auditor};
use futures::future::{BoxFuture, FutureExt};
use std::sync::Arc;
struct ConsoleAuditor;
impl Auditor for ConsoleAuditor {
fn write_audit_data(
&self,
path: &str,
data: &[u8],
) -> BoxFuture<'static, Result<(), APIClientError>> {
let path = path.to_string();
let content = String::from_utf8_lossy(data).into_owned();
async move {
println!("Audit entry [{path}]:\n{content}");
Ok(())
}
.boxed()
}
}
let client = APIClient::new("https://api.example.com".to_string())
.with_auditor(Arc::new(ConsoleAuditor))
.build()?;Audit logging is configured per-request using AuditConfig:
use api_client::AuditConfig;
// Name the audit entry and mute response body (e.g., for large responses or streaming)
let audit = AuditConfig::new("file-download").mute_response();
let response = client
.request(
"/download",
Method::Get,
Headers::new(),
None,
None,
Some(audit),
)
.await?;When streaming large response bodies, you must mute the response audit with AuditConfig::mute_response(). This
prevents the client from buffering the entire response payload into memory.
use api_client::{APIClient, AuditConfig, Headers, Method};
use futures::StreamExt;
let audit = AuditConfig::new("large-download").mute_response();
let response = client
.request(
"/stream",
Method::Get,
Headers::new(),
None,
None,
Some(audit),
)
.await?;
let mut stream = response.bytes_stream()?;
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result?;
println!("Received chunk: {} bytes", chunk.len());
}The client manages cookies across requests automatically. To reset the session and clear stored cookies:
client.clear_cookies();GPL-3.0-only