botlib/src/http_client.rs
Rodrigo Rodriguez (Pragmatismo) 34152dabc3 fix: change default botserver port from 8081 to 8080
Match botserver's default port configuration for proper
health check connectivity between botui and botserver.
2025-12-08 14:08:56 -03:00

238 lines
7.1 KiB
Rust

//! HTTP client for communicating with botserver
//!
//! Provides a reusable HTTP client for API calls.
use crate::error::BotError;
use log::{debug, error};
use serde::{de::DeserializeOwned, Serialize};
use std::sync::Arc;
use std::time::Duration;
/// HTTP client for communicating with botserver
#[derive(Clone)]
pub struct BotServerClient {
client: Arc<reqwest::Client>,
base_url: String,
}
impl BotServerClient {
/// Create new botserver HTTP client
pub fn new(base_url: Option<String>) -> Self {
let url = base_url.unwrap_or_else(|| {
std::env::var("BOTSERVER_URL").unwrap_or_else(|_| "http://localhost:8080".to_string())
});
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.user_agent(format!("BotLib/{}", env!("CARGO_PKG_VERSION")))
.build()
.expect("Failed to create HTTP client");
Self {
client: Arc::new(client),
base_url: url,
}
}
/// Create with custom timeout
pub fn with_timeout(base_url: Option<String>, timeout: Duration) -> Self {
let url = base_url.unwrap_or_else(|| {
std::env::var("BOTSERVER_URL").unwrap_or_else(|_| "http://localhost:8080".to_string())
});
let client = reqwest::Client::builder()
.timeout(timeout)
.user_agent(format!("BotLib/{}", env!("CARGO_PKG_VERSION")))
.build()
.expect("Failed to create HTTP client");
Self {
client: Arc::new(client),
base_url: url,
}
}
/// Get the base URL
pub fn base_url(&self) -> &str {
&self.base_url
}
/// GET request
pub async fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, BotError> {
let url = format!("{}{}", self.base_url, endpoint);
debug!("GET {}", url);
let response = self.client.get(&url).send().await?;
self.handle_response(response).await
}
/// POST request with body
pub async fn post<T: Serialize, R: DeserializeOwned>(
&self,
endpoint: &str,
body: &T,
) -> Result<R, BotError> {
let url = format!("{}{}", self.base_url, endpoint);
debug!("POST {}", url);
let response = self.client.post(&url).json(body).send().await?;
self.handle_response(response).await
}
/// PUT request with body
pub async fn put<T: Serialize, R: DeserializeOwned>(
&self,
endpoint: &str,
body: &T,
) -> Result<R, BotError> {
let url = format!("{}{}", self.base_url, endpoint);
debug!("PUT {}", url);
let response = self.client.put(&url).json(body).send().await?;
self.handle_response(response).await
}
/// PATCH request with body
pub async fn patch<T: Serialize, R: DeserializeOwned>(
&self,
endpoint: &str,
body: &T,
) -> Result<R, BotError> {
let url = format!("{}{}", self.base_url, endpoint);
debug!("PATCH {}", url);
let response = self.client.patch(&url).json(body).send().await?;
self.handle_response(response).await
}
/// DELETE request
pub async fn delete<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, BotError> {
let url = format!("{}{}", self.base_url, endpoint);
debug!("DELETE {}", url);
let response = self.client.delete(&url).send().await?;
self.handle_response(response).await
}
/// Handle response and deserialize
async fn handle_response<T: DeserializeOwned>(
&self,
response: reqwest::Response,
) -> Result<T, BotError> {
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
error!("HTTP {} error: {}", status, error_text);
return Err(BotError::http(format!("HTTP {}: {}", status, error_text)));
}
response.json().await.map_err(|e| {
error!("Failed to parse response: {}", e);
BotError::http(format!("Failed to parse response: {}", e))
})
}
/// Check if botserver is healthy
pub async fn health_check(&self) -> bool {
match self.get::<serde_json::Value>("/health").await {
Ok(_) => true,
Err(e) => {
error!("Health check failed: {}", e);
false
}
}
}
/// GET with bearer token authorization
pub async fn get_authorized<T: DeserializeOwned>(
&self,
endpoint: &str,
token: &str,
) -> Result<T, BotError> {
let url = format!("{}{}", self.base_url, endpoint);
debug!("GET {} (authorized)", url);
let response = self.client.get(&url).bearer_auth(token).send().await?;
self.handle_response(response).await
}
/// POST with bearer token authorization
pub async fn post_authorized<T: Serialize, R: DeserializeOwned>(
&self,
endpoint: &str,
body: &T,
token: &str,
) -> Result<R, BotError> {
let url = format!("{}{}", self.base_url, endpoint);
debug!("POST {} (authorized)", url);
let response = self
.client
.post(&url)
.bearer_auth(token)
.json(body)
.send()
.await?;
self.handle_response(response).await
}
/// DELETE with bearer token authorization
pub async fn delete_authorized<T: DeserializeOwned>(
&self,
endpoint: &str,
token: &str,
) -> Result<T, BotError> {
let url = format!("{}{}", self.base_url, endpoint);
debug!("DELETE {} (authorized)", url);
let response = self.client.delete(&url).bearer_auth(token).send().await?;
self.handle_response(response).await
}
}
impl std::fmt::Debug for BotServerClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BotServerClient")
.field("base_url", &self.base_url)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_creation() {
let client = BotServerClient::new(Some("http://localhost:8080".to_string()));
assert_eq!(client.base_url(), "http://localhost:8080");
}
#[test]
fn test_client_default_url() {
std::env::remove_var("BOTSERVER_URL");
let client = BotServerClient::new(None);
assert_eq!(client.base_url(), "http://localhost:8080");
}
#[test]
fn test_client_with_timeout() {
let client = BotServerClient::with_timeout(
Some("http://test:9000".to_string()),
Duration::from_secs(60),
);
assert_eq!(client.base_url(), "http://test:9000");
}
#[test]
fn test_client_debug() {
let client = BotServerClient::new(Some("http://debug-test".to_string()));
let debug_str = format!("{:?}", client);
assert!(debug_str.contains("BotServerClient"));
assert!(debug_str.contains("http://debug-test"));
}
}