| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580 |
- //! HTTP client wrapper
- #[cfg(feature = "bitreq")]
- use std::sync::Arc;
- #[cfg(feature = "bitreq")]
- use bitreq::RequestExt;
- use serde::de::DeserializeOwned;
- use serde::Serialize;
- use crate::error::HttpError;
- use crate::request::RequestBuilder;
- use crate::response::{RawResponse, Response};
- /// HTTP client wrapper
- #[derive(Clone)]
- pub struct HttpClient {
- #[cfg(feature = "reqwest")]
- inner: reqwest::Client,
- #[cfg(feature = "bitreq")]
- inner: Arc<bitreq::Client>,
- #[cfg(feature = "bitreq")]
- proxy_config: Option<ProxyConfig>,
- }
- impl std::fmt::Debug for HttpClient {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("HttpClient").finish()
- }
- }
- #[cfg(feature = "reqwest")]
- impl HttpClient {
- /// Create a new HTTP client with default settings
- pub fn new() -> Self {
- Self {
- inner: reqwest::Client::new(),
- }
- }
- /// Create an HttpClient from a reqwest::Client
- pub fn from_reqwest(client: reqwest::Client) -> Self {
- Self { inner: client }
- }
- /// Create a new HTTP client builder
- pub fn builder() -> HttpClientBuilder {
- HttpClientBuilder::default()
- }
- /// GET request, returns JSON deserialized to R
- pub async fn fetch<R: DeserializeOwned>(&self, url: &str) -> Response<R> {
- let response = self.inner.get(url).send().await.map_err(HttpError::from)?;
- let status = response.status();
- if !status.is_success() {
- let message = response.text().await.unwrap_or_default();
- return Err(HttpError::Status {
- status: status.as_u16(),
- message,
- });
- }
- response.json().await.map_err(HttpError::from)
- }
- /// POST with JSON body, returns JSON deserialized to R
- pub async fn post_json<B: Serialize + ?Sized, R: DeserializeOwned>(
- &self,
- url: &str,
- body: &B,
- ) -> Response<R> {
- let response = self
- .inner
- .post(url)
- .json(body)
- .send()
- .await
- .map_err(HttpError::from)?;
- let status = response.status();
- if !status.is_success() {
- let message = response.text().await.unwrap_or_default();
- return Err(HttpError::Status {
- status: status.as_u16(),
- message,
- });
- }
- response.json().await.map_err(HttpError::from)
- }
- /// POST with form data, returns JSON deserialized to R
- pub async fn post_form<F: Serialize + ?Sized, R: DeserializeOwned>(
- &self,
- url: &str,
- form: &F,
- ) -> Response<R> {
- let response = self
- .inner
- .post(url)
- .form(form)
- .send()
- .await
- .map_err(HttpError::from)?;
- let status = response.status();
- if !status.is_success() {
- let message = response.text().await.unwrap_or_default();
- return Err(HttpError::Status {
- status: status.as_u16(),
- message,
- });
- }
- response.json().await.map_err(HttpError::from)
- }
- /// PATCH with JSON body, returns JSON deserialized to R
- pub async fn patch_json<B: Serialize + ?Sized, R: DeserializeOwned>(
- &self,
- url: &str,
- body: &B,
- ) -> Response<R> {
- let response = self
- .inner
- .patch(url)
- .json(body)
- .send()
- .await
- .map_err(HttpError::from)?;
- let status = response.status();
- if !status.is_success() {
- let message = response.text().await.unwrap_or_default();
- return Err(HttpError::Status {
- status: status.as_u16(),
- message,
- });
- }
- response.json().await.map_err(HttpError::from)
- }
- /// GET request returning raw response body
- pub async fn get_raw(&self, url: &str) -> Response<RawResponse> {
- let response = self.inner.get(url).send().await.map_err(HttpError::from)?;
- Ok(RawResponse::new(response))
- }
- /// POST request builder for complex cases
- pub fn post(&self, url: &str) -> RequestBuilder {
- RequestBuilder::new(self.inner.post(url))
- }
- /// GET request builder for complex cases
- pub fn get(&self, url: &str) -> RequestBuilder {
- RequestBuilder::new(self.inner.get(url))
- }
- /// PATCH request builder for complex cases
- pub fn patch(&self, url: &str) -> RequestBuilder {
- RequestBuilder::new(self.inner.patch(url))
- }
- }
- #[cfg(feature = "bitreq")]
- impl HttpClient {
- /// Create a new HTTP client with default settings
- pub fn new() -> Self {
- Self {
- inner: Arc::new(bitreq::Client::new(10)), // Default capacity of 10
- proxy_config: None,
- }
- }
- /// Create a new HTTP client builder
- pub fn builder() -> HttpClientBuilder {
- HttpClientBuilder::default()
- }
- /// Helper method to apply proxy if URL matches the configured proxy rules
- fn apply_proxy_if_needed(
- &self,
- request: bitreq::Request,
- url: &str,
- ) -> Response<bitreq::Request> {
- apply_proxy_if_needed(request, url, &self.proxy_config)
- }
- /// GET request, returns JSON deserialized to R
- pub async fn fetch<R: DeserializeOwned>(&self, url: &str) -> Response<R> {
- let request = bitreq::get(url);
- let request = self.apply_proxy_if_needed(request, url)?;
- let response = request
- .send_async_with_client(&self.inner)
- .await
- .map_err(HttpError::from)?;
- let status = response.status_code;
- if !(200..300).contains(&status) {
- let message = response.as_str().unwrap_or("").to_string();
- return Err(HttpError::Status {
- status: status as u16,
- message,
- });
- }
- response.json().map_err(HttpError::from)
- }
- /// POST with JSON body, returns JSON deserialized to R
- pub async fn post_json<B: Serialize, R: DeserializeOwned>(
- &self,
- url: &str,
- body: &B,
- ) -> Response<R> {
- let request = bitreq::post(url).with_json(body).map_err(HttpError::from)?;
- let request = self.apply_proxy_if_needed(request, url)?;
- let response: bitreq::Response = request
- .send_async_with_client(&self.inner)
- .await
- .map_err(HttpError::from)?;
- let status = response.status_code;
- if !(200..300).contains(&status) {
- let message = response.as_str().unwrap_or("").to_string();
- return Err(HttpError::Status {
- status: status as u16,
- message,
- });
- }
- response.json().map_err(HttpError::from)
- }
- /// POST with form data, returns JSON deserialized to R
- pub async fn post_form<F: Serialize, R: DeserializeOwned>(
- &self,
- url: &str,
- form: &F,
- ) -> Response<R> {
- let form_str = serde_urlencoded::to_string(form)
- .map_err(|e| HttpError::Serialization(e.to_string()))?;
- let request = bitreq::post(url)
- .with_body(form_str.into_bytes())
- .with_header("Content-Type", "application/x-www-form-urlencoded");
- let request = self.apply_proxy_if_needed(request, url)?;
- let response: bitreq::Response = request
- .send_async_with_client(&self.inner)
- .await
- .map_err(HttpError::from)?;
- let status = response.status_code;
- if !(200..300).contains(&status) {
- let message = response.as_str().unwrap_or("").to_string();
- return Err(HttpError::Status {
- status: status as u16,
- message,
- });
- }
- response.json().map_err(HttpError::from)
- }
- /// PATCH with JSON body, returns JSON deserialized to R
- pub async fn patch_json<B: Serialize, R: DeserializeOwned>(
- &self,
- url: &str,
- body: &B,
- ) -> Response<R> {
- let request = bitreq::patch(url)
- .with_json(body)
- .map_err(HttpError::from)?;
- let request = self.apply_proxy_if_needed(request, url)?;
- let response: bitreq::Response = request
- .send_async_with_client(&self.inner)
- .await
- .map_err(HttpError::from)?;
- let status = response.status_code;
- if !(200..300).contains(&status) {
- let message = response.as_str().unwrap_or("").to_string();
- return Err(HttpError::Status {
- status: status as u16,
- message,
- });
- }
- response.json().map_err(HttpError::from)
- }
- /// GET request returning raw response body
- pub async fn get_raw(&self, url: &str) -> Response<RawResponse> {
- let request = bitreq::get(url);
- let request = self.apply_proxy_if_needed(request, url)?;
- let response = request
- .send_async_with_client(&self.inner)
- .await
- .map_err(HttpError::from)?;
- Ok(RawResponse::new(response))
- }
- /// POST request builder for complex cases
- pub fn post(&self, url: &str) -> RequestBuilder {
- // Note: Proxy will be applied when the request is sent
- RequestBuilder::new(
- bitreq::post(url),
- url,
- self.inner.clone(),
- self.proxy_config.clone(),
- )
- }
- /// GET request builder for complex cases
- pub fn get(&self, url: &str) -> RequestBuilder {
- RequestBuilder::new(
- bitreq::get(url),
- url,
- self.inner.clone(),
- self.proxy_config.clone(),
- )
- }
- /// PATCH request builder for complex cases
- pub fn patch(&self, url: &str) -> RequestBuilder {
- RequestBuilder::new(
- bitreq::patch(url),
- url,
- self.inner.clone(),
- self.proxy_config.clone(),
- )
- }
- }
- impl Default for HttpClient {
- fn default() -> Self {
- Self::new()
- }
- }
- /// HTTP client builder for configuring proxy and TLS settings
- #[derive(Debug, Default)]
- pub struct HttpClientBuilder {
- #[cfg(any(feature = "reqwest", feature = "bitreq"))]
- proxy: Option<ProxyConfig>,
- #[cfg(feature = "bitreq")]
- accept_invalid_certs: bool,
- }
- #[cfg(any(feature = "bitreq", feature = "reqwest"))]
- #[derive(Debug, Clone)]
- pub(crate) struct ProxyConfig {
- url: url::Url,
- matcher: Option<regex::Regex>,
- }
- #[cfg(feature = "bitreq")]
- pub(crate) fn apply_proxy_if_needed(
- request: bitreq::Request,
- url: &str,
- proxy_config: &Option<ProxyConfig>,
- ) -> Response<bitreq::Request> {
- if let Some(ref config) = proxy_config {
- if let Some(ref matcher) = config.matcher {
- // Check if URL host matches the regex pattern
- if let Ok(parsed_url) = url::Url::parse(url) {
- if let Some(host) = parsed_url.host_str() {
- if matcher.is_match(host) {
- let proxy = bitreq::Proxy::new_http(&config.url)
- .map_err(|e| HttpError::Proxy(e.to_string()))?;
- return Ok(request.with_proxy(proxy));
- }
- }
- }
- } else {
- // No matcher, apply proxy to all requests
- let proxy = bitreq::Proxy::new_http(&config.url)
- .map_err(|e| HttpError::Proxy(e.to_string()))?;
- return Ok(request.with_proxy(proxy));
- }
- }
- Ok(request)
- }
- impl HttpClientBuilder {
- /// Set a proxy URL (reqwest only)
- #[cfg(feature = "reqwest")]
- pub fn proxy(mut self, url: url::Url) -> Self {
- self.proxy = Some(ProxyConfig { url, matcher: None });
- self
- }
- /// Set a proxy URL with a host pattern matcher (reqwest only)
- #[cfg(feature = "reqwest")]
- pub fn proxy_with_matcher(mut self, url: url::Url, pattern: &str) -> Response<Self> {
- let matcher = regex::Regex::new(pattern)
- .map_err(|e| HttpError::Proxy(format!("Invalid proxy pattern: {}", e)))?;
- self.proxy = Some(ProxyConfig {
- url,
- matcher: Some(matcher),
- });
- Ok(self)
- }
- /// Accept invalid TLS certificates (bitreq only)
- #[cfg(feature = "bitreq")]
- pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
- self.accept_invalid_certs = accept;
- self
- }
- /// Set a proxy URL (bitreq only)
- #[cfg(feature = "bitreq")]
- pub fn proxy(mut self, url: url::Url) -> Self {
- self.proxy = Some(ProxyConfig { url, matcher: None });
- self
- }
- /// Set a proxy URL with a host pattern matcher (bitreq only)
- #[cfg(feature = "bitreq")]
- pub fn proxy_with_matcher(mut self, url: url::Url, pattern: &str) -> Response<Self> {
- let matcher = regex::Regex::new(pattern)
- .map_err(|e| HttpError::Proxy(format!("Invalid proxy pattern: {}", e)))?;
- self.proxy = Some(ProxyConfig {
- url,
- matcher: Some(matcher),
- });
- Ok(self)
- }
- /// Build the HTTP client
- pub fn build(self) -> Response<HttpClient> {
- #[cfg(feature = "reqwest")]
- {
- let mut builder = reqwest::Client::builder();
- if let Some(proxy) = self.proxy {
- let proxy_url = proxy.url;
- if let Some(matcher) = proxy.matcher {
- let custom_proxy = reqwest::Proxy::custom(move |url| {
- url.host_str().and_then(|host| {
- if matcher.is_match(host) {
- Some(proxy_url.clone())
- } else {
- None
- }
- })
- });
- builder = builder.proxy(custom_proxy);
- } else {
- let proxy = reqwest::Proxy::all(proxy_url)
- .map_err(|e| HttpError::Proxy(e.to_string()))?;
- builder = builder.proxy(proxy);
- }
- }
- let client = builder
- .build()
- .map_err(|e| HttpError::Build(e.to_string()))?;
- Ok(HttpClient { inner: client })
- }
- #[cfg(feature = "bitreq")]
- {
- // Return error if danger_accept_invalid_certs was set
- if self.accept_invalid_certs {
- return Err(HttpError::Build(
- "danger_accept_invalid_certs is not supported".to_string(),
- ));
- }
- Ok(HttpClient {
- inner: Arc::new(bitreq::Client::new(10)), // Default capacity of 10
- proxy_config: self.proxy,
- })
- }
- }
- }
- /// Convenience function for simple GET requests
- pub async fn fetch<R: DeserializeOwned>(url: &str) -> Response<R> {
- HttpClient::new().fetch(url).await
- }
- #[cfg(test)]
- mod tests {
- use super::*;
- #[test]
- fn test_client_new() {
- let client = HttpClient::new();
- // Client should be constructable without panicking
- let _ = format!("{:?}", client);
- }
- #[test]
- fn test_client_default() {
- let client = HttpClient::default();
- // Default should produce a valid client
- let _ = format!("{:?}", client);
- }
- #[test]
- fn test_builder_returns_builder() {
- let builder = HttpClient::builder();
- let _ = format!("{:?}", builder);
- }
- #[test]
- fn test_builder_build() {
- let result = HttpClientBuilder::default().build();
- assert!(result.is_ok());
- }
- #[cfg(feature = "reqwest")]
- #[test]
- fn test_from_reqwest() {
- let reqwest_client = reqwest::Client::new();
- let client = HttpClient::from_reqwest(reqwest_client);
- let _ = format!("{:?}", client);
- }
- #[cfg(feature = "bitreq")]
- mod bitreq_tests {
- use super::*;
- #[test]
- fn test_builder_accept_invalid_certs_returns_error() {
- let result = HttpClientBuilder::default()
- .danger_accept_invalid_certs(true)
- .build();
- assert!(result.is_err());
- if let Err(HttpError::Build(msg)) = result {
- assert!(msg.contains("danger_accept_invalid_certs"));
- } else {
- panic!("Expected HttpError::Build");
- }
- }
- #[test]
- fn test_builder_accept_invalid_certs_false_ok() {
- let result = HttpClientBuilder::default()
- .danger_accept_invalid_certs(false)
- .build();
- assert!(result.is_ok());
- }
- #[test]
- fn test_builder_proxy() {
- let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
- let result = HttpClientBuilder::default().proxy(proxy_url).build();
- assert!(result.is_ok());
- }
- #[test]
- fn test_builder_proxy_with_valid_matcher() {
- let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
- let result =
- HttpClientBuilder::default().proxy_with_matcher(proxy_url, r".*\.example\.com$");
- assert!(result.is_ok());
- let builder = result.expect("Valid matcher should succeed");
- let client_result = builder.build();
- assert!(client_result.is_ok());
- }
- #[test]
- fn test_builder_proxy_with_invalid_matcher() {
- let proxy_url = url::Url::parse("http://localhost:8080").expect("Valid proxy URL");
- // Invalid regex pattern (unclosed bracket)
- let result = HttpClientBuilder::default().proxy_with_matcher(proxy_url, r"[invalid");
- assert!(result.is_err());
- if let Err(HttpError::Proxy(msg)) = result {
- assert!(msg.contains("Invalid proxy pattern"));
- } else {
- panic!("Expected HttpError::Proxy");
- }
- }
- }
- }
|