Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,497 changes: 0 additions & 1,497 deletions ccxt-core/src/http_client.rs

This file was deleted.

172 changes: 172 additions & 0 deletions ccxt-core/src/http_client/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use crate::circuit_breaker::CircuitBreaker;
use crate::error::{Error, Result};
use crate::rate_limiter::RateLimiter;
use crate::retry_strategy::RetryStrategy;
use reqwest::Client;
use std::sync::Arc;

use super::config::HttpConfig;

/// HTTP client with retry and rate limiting support
#[derive(Debug)]
pub struct HttpClient {
client: Client,
config: HttpConfig,
rate_limiter: Option<RateLimiter>,
retry_strategy: RetryStrategy,
circuit_breaker: Option<Arc<CircuitBreaker>>,
}

impl HttpClient {
/// Creates a new HTTP client with the given configuration.
///
/// # Arguments
///
/// * `config` - HTTP client configuration
///
/// # Returns
///
/// Returns a `Result` containing the initialized client or an error if client creation fails.
///
/// # Errors
///
/// Returns an error if:
/// - The proxy URL is invalid
/// - The HTTP client cannot be built
pub fn new(config: HttpConfig) -> Result<Self> {
let mut builder = Client::builder()
.timeout(config.timeout)
.connect_timeout(config.connect_timeout)
.pool_max_idle_per_host(config.pool_max_idle_per_host)
.pool_idle_timeout(config.pool_idle_timeout)
.gzip(true)
.user_agent(&config.user_agent);

if let Some(proxy_config) = &config.proxy {
let mut proxy = reqwest::Proxy::all(&proxy_config.url)
.map_err(|e| Error::network(format!("Invalid proxy URL: {e}")))?;

if let (Some(username), Some(password)) =
(&proxy_config.username, &proxy_config.password)
{
proxy = proxy.basic_auth(username, password);
}
builder = builder.proxy(proxy);
}

let client = builder
.build()
.map_err(|e| Error::network(format!("Failed to build HTTP client: {e}")))?;

let retry_strategy = RetryStrategy::new(config.retry_config.clone().unwrap_or_default());

let circuit_breaker = config
.circuit_breaker
.as_ref()
.map(|cb_config| Arc::new(CircuitBreaker::new(cb_config.clone())));

Ok(Self {
client,
config,
rate_limiter: None,
retry_strategy,
circuit_breaker,
})
}

/// Creates a new HTTP client with a custom rate limiter.
///
/// # Arguments
///
/// * `config` - HTTP client configuration
/// * `rate_limiter` - Pre-configured rate limiter instance
///
/// # Returns
///
/// Returns a `Result` containing the initialized client with rate limiter.
///
/// # Errors
///
/// Returns an error if client creation fails.
pub fn new_with_rate_limiter(config: HttpConfig, rate_limiter: RateLimiter) -> Result<Self> {
let mut client = Self::new(config)?;
client.rate_limiter = Some(rate_limiter);
Ok(client)
}

/// Sets the rate limiter for this client.
///
/// # Arguments
///
/// * `rate_limiter` - Rate limiter instance to use
pub fn set_rate_limiter(&mut self, rate_limiter: RateLimiter) {
self.rate_limiter = Some(rate_limiter);
}

/// Sets the retry strategy for this client.
///
/// # Arguments
///
/// * `strategy` - Retry strategy to use for failed requests
pub fn set_retry_strategy(&mut self, strategy: RetryStrategy) {
self.retry_strategy = strategy;
}

/// Sets the circuit breaker for this client.
///
/// # Arguments
///
/// * `circuit_breaker` - Circuit breaker instance to use
///
/// # Example
///
/// ```rust
/// use ccxt_core::http_client::{HttpClient, HttpConfig};
/// use ccxt_core::circuit_breaker::{CircuitBreaker, CircuitBreakerConfig};
/// use std::sync::Arc;
///
/// let mut client = HttpClient::new(HttpConfig::default()).unwrap();
/// let cb = Arc::new(CircuitBreaker::new(CircuitBreakerConfig::default()));
/// client.set_circuit_breaker(cb);
/// ```
pub fn set_circuit_breaker(&mut self, circuit_breaker: Arc<CircuitBreaker>) {
self.circuit_breaker = Some(circuit_breaker);
}

/// Returns a reference to the circuit breaker, if configured.
///
/// This can be used to check the circuit breaker state or manually
/// record success/failure.
pub fn circuit_breaker(&self) -> Option<&Arc<CircuitBreaker>> {
self.circuit_breaker.as_ref()
}

/// Returns a reference to current HTTP configuration.
pub fn config(&self) -> &HttpConfig {
&self.config
}

/// Updates the HTTP configuration.
///
/// # Arguments
///
/// * `config` - New configuration to use
pub fn set_config(&mut self, config: HttpConfig) {
self.config = config;
}

/// Internal: Returns reference to the underlying reqwest client.
pub(crate) fn client(&self) -> &Client {
&self.client
}

/// Internal: Returns reference to the rate limiter, if configured.
pub(crate) fn rate_limiter(&self) -> Option<&RateLimiter> {
self.rate_limiter.as_ref()
}

/// Internal: Returns reference to the retry strategy.
pub(crate) fn retry_strategy(&self) -> &RetryStrategy {
&self.retry_strategy
}
}
159 changes: 159 additions & 0 deletions ccxt-core/src/http_client/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use crate::circuit_breaker::CircuitBreakerConfig;
use crate::config::ProxyConfig;
use crate::error::{ConfigValidationError, ValidationResult};
use crate::retry_strategy::RetryConfig;
use std::time::Duration;

/// HTTP request configuration
#[derive(Debug, Clone)]
pub struct HttpConfig {
/// Request timeout
pub timeout: Duration,
/// TCP connection timeout (default: 10 seconds)
pub connect_timeout: Duration,
/// Maximum retry attempts (deprecated, use `retry_config` instead)
#[deprecated(note = "Use retry_config instead")]
pub max_retries: u32,
/// Whether to enable verbose logging
pub verbose: bool,
/// Default User-Agent header value
pub user_agent: String,
/// Whether to include response headers in the result
pub return_response_headers: bool,
/// Optional proxy configuration
pub proxy: Option<ProxyConfig>,
/// Whether to enable rate limiting
pub enable_rate_limit: bool,
/// Optional retry configuration (uses default if `None`)
pub retry_config: Option<RetryConfig>,
/// Maximum response body size in bytes (default: 10MB)
///
/// Responses exceeding this limit will be rejected with an `InvalidRequest` error.
/// This protects against malicious or abnormal responses that could exhaust memory.
pub max_response_size: usize,

/// Maximum request body size in bytes (default: 10MB)
///
/// Request bodies exceeding this limit will be rejected BEFORE serialization.
/// This protects against DoS attacks via oversized request payloads.
pub max_request_size: usize,

/// Optional circuit breaker configuration.
///
/// When enabled, the circuit breaker will track request failures and
/// automatically block requests to failing endpoints, allowing the
/// system to recover.
///
/// Default: `None` (disabled for backward compatibility)
pub circuit_breaker: Option<CircuitBreakerConfig>,

/// Maximum number of idle connections per host in the connection pool.
///
/// This controls how many keep-alive connections are maintained for each host.
/// Higher values improve performance for repeated requests to the same host
/// but consume more resources.
///
/// Default: 10
pub pool_max_idle_per_host: usize,

/// Timeout for idle connections in the pool.
///
/// Connections that have been idle longer than this duration will be closed.
/// This helps free up resources and avoid stale connections.
///
/// Default: 90 seconds
pub pool_idle_timeout: Duration,
}

impl Default for HttpConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
connect_timeout: Duration::from_secs(10),
#[allow(deprecated)]
max_retries: 3,
verbose: false,
user_agent: "ccxt-rust/1.0".to_string(),
return_response_headers: false,
proxy: None,
enable_rate_limit: true,
retry_config: None,
max_response_size: 10 * 1024 * 1024,
max_request_size: 10 * 1024 * 1024,
circuit_breaker: None,
pool_max_idle_per_host: 10,
pool_idle_timeout: Duration::from_secs(90),
}
}
}

impl HttpConfig {
/// Validates the HTTP configuration parameters.
///
/// # Returns
///
/// Returns `Ok(ValidationResult)` if the configuration is valid.
/// The `ValidationResult` may contain warnings for suboptimal but valid configurations.
///
/// Returns `Err(ConfigValidationError)` if the configuration is invalid.
///
/// # Validation Rules
///
/// - `timeout` > 5 minutes returns an error (excessive timeout)
/// - `timeout` < 1 second generates a warning (may cause frequent timeouts)
///
/// # Example
///
/// ```rust
/// use ccxt_core::http_client::HttpConfig;
/// use std::time::Duration;
///
/// let config = HttpConfig::default();
/// let result = config.validate();
/// assert!(result.is_ok());
///
/// let invalid_config = HttpConfig {
/// timeout: Duration::from_secs(600), // 10 minutes - too long
/// ..Default::default()
/// };
/// let result = invalid_config.validate();
/// assert!(result.is_err());
/// ```
pub fn validate(&self) -> std::result::Result<ValidationResult, ConfigValidationError> {
const MAX_REASONABLE_REQUEST_SIZE: usize = 100 * 1024 * 1024;

let mut warnings = Vec::new();
let max_timeout = Duration::from_secs(300);
if self.timeout > max_timeout {
return Err(ConfigValidationError::too_high(
"timeout",
format!("{:?}", self.timeout),
"5 minutes",
));
}

if self.timeout < Duration::from_secs(1) {
warnings.push(format!(
"timeout {:?} is very short, may cause frequent timeouts",
self.timeout
));
}

if self.max_request_size == 0 {
return Err(ConfigValidationError::invalid(
"max_request_size",
"max_request_size cannot be zero",
));
}

if self.max_request_size > MAX_REASONABLE_REQUEST_SIZE {
return Err(ConfigValidationError::too_high(
"max_request_size",
format!("{}", self.max_request_size),
"100MB (104857600 bytes)",
));
}

Ok(ValidationResult::with_warnings(warnings))
}
}
12 changes: 12 additions & 0 deletions ccxt-core/src/http_client/headers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use reqwest::header::HeaderMap;
use serde_json::Value;

pub(crate) fn headers_to_json(headers: &HeaderMap) -> Value {
let mut map = serde_json::Map::new();
for (key, value) in headers {
let key_str = key.as_str().to_string();
let value_str = value.to_str().unwrap_or("").to_string();
map.insert(key_str, Value::String(value_str));
}
Value::Object(map)
}
39 changes: 39 additions & 0 deletions ccxt-core/src/http_client/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! HTTP client abstraction layer
//!
//! Provides a unified HTTP request interface with support for:
//! - Automatic retry mechanism with configurable strategies
//! - Timeout control per request
//! - Request and response size limits for DoS protection
//! - Circuit breaker integration for resilience
//! - Rate limiting integration
//!
//! # Example
//!
//! ```rust,no_run
//! use ccxt_core::http_client::{HttpClient, HttpConfig};
//!
//! let config = HttpConfig::default();
//! let client = HttpClient::new(config)?;
//!
//! let response = client.get("https://example.com/api", None).await?;
//! ```
//!
//! # Features
//!
//! - **Retry**: Automatic retry with exponential backoff for transient failures
//! - **Circuit Breaker**: Prevents cascading failures by blocking requests to failing endpoints
//! - **Rate Limiting**: Optional rate limiting for API quota management
//! - **Size Limits**: Protection against oversized requests/responses

mod builder;
mod config;
mod headers;
mod request;
mod response;
mod retry;

#[cfg(test)]
mod tests;

pub use builder::HttpClient;
pub use config::HttpConfig;
Loading