use crate::core::error::RustChainError;
use futures::StreamExt;
use reqwest::{Client, Response};
use std::time::Duration;
pub const DEFAULT_TIMEOUT_SECS: u64 = 30;
pub const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 10;
pub const MAX_ERROR_BODY_SIZE: usize = 64 * 1024;
pub const MAX_RESPONSE_BODY_SIZE: usize = 10 * 1024 * 1024;
pub fn create_http_client(timeout_secs: Option<u64>) -> Client {
let timeout = Duration::from_secs(timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS));
let connect_timeout = Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS);
Client::builder()
.timeout(timeout)
.connect_timeout(connect_timeout)
.redirect(reqwest::redirect::Policy::limited(10))
.build()
.expect("Failed to build HTTP client - check TLS backend availability")
}
pub fn try_create_http_client(timeout_secs: Option<u64>) -> Result<Client, RustChainError> {
let timeout = Duration::from_secs(timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS));
let connect_timeout = Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS);
Client::builder()
.timeout(timeout)
.connect_timeout(connect_timeout)
.redirect(reqwest::redirect::Policy::limited(10))
.build()
.map_err(|e| {
RustChainError::Config(crate::core::error::ConfigError::PluginError {
message: format!("Failed to create HTTP client: {}", e),
})
})
}
pub fn create_http_client_custom(request_timeout_secs: u64, connect_timeout_secs: u64) -> Client {
Client::builder()
.timeout(Duration::from_secs(request_timeout_secs))
.connect_timeout(Duration::from_secs(connect_timeout_secs))
.redirect(reqwest::redirect::Policy::limited(10))
.build()
.expect("Failed to build HTTP client - check TLS backend availability")
}
pub async fn read_response_body_bounded(
response: Response,
max_size: Option<usize>,
) -> Result<String, reqwest::Error> {
let limit = max_size.unwrap_or(MAX_RESPONSE_BODY_SIZE);
if let Some(content_length) = response.content_length() {
if content_length as usize > limit * 2 {
return Ok(format!(
"[Response too large: {} bytes, limit {} bytes]",
content_length, limit
));
}
}
let mut stream = response.bytes_stream();
let mut buffer = Vec::with_capacity(limit.min(1024 * 1024)); let mut total_size: usize = 0;
let mut was_truncated = false;
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result?;
let remaining = limit.saturating_sub(buffer.len());
if remaining == 0 {
total_size += chunk.len();
was_truncated = true;
continue;
}
if chunk.len() <= remaining {
buffer.extend_from_slice(&chunk);
} else {
buffer.extend_from_slice(&chunk[..remaining]);
total_size = buffer.len() + (chunk.len() - remaining);
was_truncated = true;
}
}
if !was_truncated {
total_size = buffer.len();
}
let body = String::from_utf8_lossy(&buffer).into_owned();
if was_truncated {
Ok(format!(
"{}... [truncated at {} bytes, total ~{} bytes]",
body,
limit,
total_size + limit
))
} else {
Ok(body)
}
}
pub async fn read_error_body_bounded(response: Response) -> String {
match read_response_body_bounded(response, Some(MAX_ERROR_BODY_SIZE)).await {
Ok(body) => body,
Err(e) => format!("[Failed to read error body: {}]", e),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_http_client_default() {
let client = create_http_client(None);
assert!(client.get("http://example.com").build().is_ok());
}
#[test]
fn test_create_http_client_custom_timeout() {
let client = create_http_client(Some(60));
assert!(client.get("http://example.com").build().is_ok());
}
#[test]
fn test_create_http_client_custom_timeouts() {
let client = create_http_client_custom(120, 30);
assert!(client.get("http://example.com").build().is_ok());
}
}