rskit-httpclient 0.2.0-alpha.1

Async HTTP client with auth and injected resilience policies for rskit
Documentation
# rskit-httpclient

Async HTTP client for rskit with redacting auth, headers, injected resilience policies, destination hardening, and error handling.

## Features

- Async HTTP client built on `reqwest`
- Support for Bearer, Basic, and API key authentication with redacted secret storage
- Configurable timeouts, headers, and redirect behavior
- Explicit TLS trust, identity, and minimum-version configuration via `rskit-security`
- Optional `rskit-resilience::Policy` integration for retry, timeout, circuit breaker, and rate limiting
- URL building with base URL support and outbound destination validation
- Bounded response body reads to avoid unbounded memory growth
- JSON request/response serialization via `serde`
- Integrated error handling with `rskit-errors`
- Request builder pattern for fluent API

## Usage

```rust
use rskit_httpclient::{HttpClient, HttpClientConfig, Request};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create client with configuration
    let config = HttpClientConfig::new()
        .with_base_url("https://api.example.com/v1")
        .with_user_agent("my-app/1.0")
        .with_header("X-Custom", "value");

    let client = HttpClient::new(config)?;

    // Simple GET
    let resp = client.get("/users").await?;
    let text = resp.text()?;

    // GET with JSON response
    let data: serde_json::Value = client.get_json("/users").await?;

    // POST with JSON body
    let body = serde_json::json!({"name": "Alice"});
    let resp = client.post("/users", &body).await?;

    // Request with custom auth
    let resp = client.send(
        Request::post("/protected")
            .bearer_token("secret-token")
            .json_body(&body)?
    ).await?;

    // Check status and parse
    let result = resp.error_for_status()?.json::<serde_json::Value>()?;

    Ok(())
}
```

## Authentication and redaction

```rust
use rskit_httpclient::{Auth, HttpClientConfig};
use rskit_security::SecretString;

// Bearer token
let auth = Auth::bearer("token123");

// HTTP Basic
let auth = Auth::basic("user", "pass");

// API Key
let auth = Auth::api_key("X-API-Key", "key123");

// Use *_secret constructors when a secret is already held as a SecretString.
let token = SecretString::new("token123");
let auth = Auth::bearer_secret(token);

// Auth and HttpClientConfig debug output redact credential values.
let config = HttpClientConfig::new().with_auth(auth);
assert!(!format!("{config:?}").contains("token123"));

// Per-request override
let resp = client.send(
    Request::get("/api/data")
        .bearer_token("request-specific-token")
).await?;
```

`Auth::header()` intentionally exposes credential values only at the final request-application boundary. Do not use `with_header` for bearer/API-key credentials; prefer `with_auth` or per-request auth helpers so debug output keeps credential fields redacted.

## Transport hardening

`HttpClientConfig` applies safe transport defaults:

- redirects are capped by `max_redirects` (default: 5)
- response bodies are capped by `max_response_body_bytes` (default: 10 MiB)
- `DestinationPolicy` validates initial request URLs and redirect targets
- link-local address literals and common cloud metadata endpoints are blocked by default

Use an allow-list for clients that should only call known hosts:

```rust
use rskit_httpclient::{DestinationPolicy, HttpClientConfig};

let config = HttpClientConfig::new()
    .with_base_url("https://api.example.com")
    .with_destination_policy(
        DestinationPolicy::new().with_allowed_hosts(["api.example.com"])
    )
    .with_max_response_body_bytes(2 * 1024 * 1024);
```

Hostname validation happens before DNS resolution, so host allow-lists are the preferred protection for high-trust clients that must not follow arbitrary destination names.

## Error Handling

All methods return `AppResult<T>` (alias for `Result<T, AppError>`). Errors are classified with appropriate `ErrorCode` values:

- `Timeout` for request timeouts
- `ConnectionFailed` for connection errors
- `Unauthorized` for 401 responses
- `Forbidden` for 403 responses
- `NotFound` for 404 responses
- And more...

```rust
match client.get("/users").await {
    Ok(resp) => { /* handle success */ },
    Err(e) => {
        println!("Error code: {}", e.code());
        println!("Message: {}", e.message());
    }
}
```