use async_trait::async_trait;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum HttpClientError {
#[error("request failed: {0}")]
RequestFailed(String),
#[error("response parse error: {0}")]
ParseError(String),
#[error("invalid status: {status} - {message}")]
InvalidStatus {
status: u16,
message: String,
},
#[error("request timeout")]
Timeout,
#[error("operation not supported: {0}")]
NotSupported(String),
}
#[async_trait]
pub trait HttpClient: Send + Sync {
async fn get_value(&self, url: &str) -> Result<serde_json::Value, HttpClientError>;
async fn post_form_value(
&self,
url: &str,
form: &[(String, String)],
auth_header: Option<(&str, &str)>,
) -> Result<serde_json::Value, HttpClientError>;
async fn post_json_value(
&self,
url: &str,
body: &serde_json::Value,
auth_header: Option<(&str, &str)>,
) -> Result<serde_json::Value, HttpClientError>;
}
#[cfg(all(not(target_arch = "wasm32"), feature = "http-reqwest"))]
pub use reqwest_impl::ReqwestHttpClient;
#[cfg(all(not(target_arch = "wasm32"), feature = "http-reqwest"))]
mod reqwest_impl {
use super::*;
use reqwest::Client;
use std::time::Duration;
#[derive(Clone)]
pub struct ReqwestHttpClient {
client: Client,
}
impl ReqwestHttpClient {
pub fn new() -> Result<Self, HttpClientError> {
let client = Client::builder()
.timeout(Duration::from_secs(30))
.use_rustls_tls()
.cookie_store(true)
.build()
.map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;
Ok(Self { client })
}
pub fn with_timeout(timeout_secs: u64) -> Result<Self, HttpClientError> {
let client = Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.use_rustls_tls()
.cookie_store(true)
.build()
.map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;
Ok(Self { client })
}
}
impl Default for ReqwestHttpClient {
fn default() -> Self {
Self::new().expect("Failed to create default HTTP client")
}
}
#[async_trait]
impl HttpClient for ReqwestHttpClient {
async fn get_value(&self, url: &str) -> Result<serde_json::Value, HttpClientError> {
let start = std::time::Instant::now();
let response = self.client.get(url).send().await.map_err(|e| {
tracing::error!(
target: "xjp_oidc::http",
url = %url,
error = %e,
"HTTP GET 请求失败"
);
if e.is_timeout() {
HttpClientError::Timeout
} else {
HttpClientError::RequestFailed(e.to_string())
}
})?;
let status = response.status();
let duration = start.elapsed();
tracing::info!(
target: "xjp_oidc::http",
url = %url,
method = "GET",
duration_ms = duration.as_millis() as u64,
status = status.as_u16(),
"HTTP 请求完成"
);
if !status.is_success() {
let message =
response.text().await.unwrap_or_else(|_| "No error message".to_string());
return Err(HttpClientError::InvalidStatus { status: status.as_u16(), message });
}
response
.json::<serde_json::Value>()
.await
.map_err(|e| HttpClientError::ParseError(e.to_string()))
}
async fn post_form_value(
&self,
url: &str,
form: &[(String, String)],
auth_header: Option<(&str, &str)>,
) -> Result<serde_json::Value, HttpClientError> {
let mut request = self.client.post(url).form(form);
if let Some((name, value)) = auth_header {
request = request.header(name, value);
}
let response = request.send().await.map_err(|e| {
if e.is_timeout() {
HttpClientError::Timeout
} else {
HttpClientError::RequestFailed(e.to_string())
}
})?;
let status = response.status();
if !status.is_success() {
let message =
response.text().await.unwrap_or_else(|_| "No error message".to_string());
return Err(HttpClientError::InvalidStatus { status: status.as_u16(), message });
}
response
.json::<serde_json::Value>()
.await
.map_err(|e| HttpClientError::ParseError(e.to_string()))
}
async fn post_json_value(
&self,
url: &str,
body: &serde_json::Value,
auth_header: Option<(&str, &str)>,
) -> Result<serde_json::Value, HttpClientError> {
let mut request = self.client.post(url).json(body);
if let Some((name, value)) = auth_header {
request = request.header(name, value);
}
let response = request.send().await.map_err(|e| {
if e.is_timeout() {
HttpClientError::Timeout
} else {
HttpClientError::RequestFailed(e.to_string())
}
})?;
let status = response.status();
if !status.is_success() {
let message =
response.text().await.unwrap_or_else(|_| "No error message".to_string());
return Err(HttpClientError::InvalidStatus { status: status.as_u16(), message });
}
response
.json::<serde_json::Value>()
.await
.map_err(|e| HttpClientError::ParseError(e.to_string()))
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "http-wasm"))]
pub use wasm_impl::WasmHttpClient;
#[cfg(all(target_arch = "wasm32", feature = "http-wasm"))]
mod wasm_impl {
use super::*;
use gloo_net::http::Request;
use web_sys::RequestCredentials;
pub struct WasmHttpClient;
impl WasmHttpClient {
pub fn new() -> Self {
Self
}
}
impl Default for WasmHttpClient {
fn default() -> Self {
Self::new()
}
}
#[async_trait(?Send)]
impl HttpClient for WasmHttpClient {
async fn get_value(&self, url: &str) -> Result<serde_json::Value, HttpClientError> {
let response = Request::get(url)
.credentials(RequestCredentials::Include)
.send()
.await
.map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;
if !response.ok() {
return Err(HttpClientError::InvalidStatus {
status: response.status(),
message: response
.text()
.await
.unwrap_or_else(|_| "No error message".to_string()),
});
}
response
.json::<serde_json::Value>()
.await
.map_err(|e| HttpClientError::ParseError(e.to_string()))
}
async fn post_form_value(
&self,
url: &str,
form: &[(String, String)],
auth_header: Option<(&str, &str)>,
) -> Result<serde_json::Value, HttpClientError> {
let mut request = Request::post(url).credentials(RequestCredentials::Include);
if let Some((name, value)) = auth_header {
request = request.header(name, value);
}
let form_body = form
.iter()
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("&");
let response = request
.header("Content-Type", "application/x-www-form-urlencoded")
.body(form_body)
.send()
.await
.map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;
if !response.ok() {
return Err(HttpClientError::InvalidStatus {
status: response.status(),
message: response
.text()
.await
.unwrap_or_else(|_| "No error message".to_string()),
});
}
response
.json::<serde_json::Value>()
.await
.map_err(|e| HttpClientError::ParseError(e.to_string()))
}
async fn post_json_value(
&self,
url: &str,
body: &serde_json::Value,
auth_header: Option<(&str, &str)>,
) -> Result<serde_json::Value, HttpClientError> {
let mut request = Request::post(url).credentials(RequestCredentials::Include);
if let Some((name, value)) = auth_header {
request = request.header(name, value);
}
let response = request
.json(body)
.map_err(|e| HttpClientError::RequestFailed(e.to_string()))?
.send()
.await
.map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;
if !response.ok() {
return Err(HttpClientError::InvalidStatus {
status: response.status(),
message: response
.text()
.await
.unwrap_or_else(|_| "No error message".to_string()),
});
}
response
.json::<serde_json::Value>()
.await
.map_err(|e| HttpClientError::ParseError(e.to_string()))
}
}
}