nyquest-backend-reqwest 0.2.1

reqwest backend for nyquest HTTP client library
Documentation
#[cfg(not(target_arch = "wasm32"))]
use std::sync::{Arc, OnceLock};

use http::{HeaderMap, HeaderName, HeaderValue};
use nyquest_interface::{client::ClientOptions, Result as NyquestResult};
use reqwest::Client;
use url::Url;

use crate::error::{ReqwestBackendError, Result};

#[derive(Clone)]
pub struct ReqwestClient {
    pub(crate) client: Client,
    pub(crate) base_url: Option<Url>,
    pub(crate) max_response_buffer_size: Option<u64>,
    #[cfg(target_arch = "wasm32")]
    pub(crate) wasm_options: crate::wasm::WasmOptions,
    #[cfg(not(target_arch = "wasm32"))]
    pub(crate) managed_runtime: Arc<OnceLock<tokio::runtime::Runtime>>,
}

impl ReqwestClient {
    pub fn new(options: ClientOptions) -> NyquestResult<Self> {
        let client = build_reqwest_client(&options)?;

        let base_url = if let Some(ref base_url_str) = options.base_url {
            Some(Url::parse(base_url_str).map_err(|_| {
                nyquest_interface::Error::Io(std::io::Error::new(
                    std::io::ErrorKind::InvalidInput,
                    format!("Invalid base URL: {}", base_url_str),
                ))
            })?)
        } else {
            None
        };

        Ok(Self {
            client,
            base_url,
            max_response_buffer_size: options.max_response_buffer_size,
            #[cfg(target_arch = "wasm32")]
            wasm_options: crate::wasm::WasmOptions {
                request_timeout: options.request_timeout,
                caching_behavior: options.caching_behavior,
                use_cookies: options.use_cookies,
            },
            #[cfg(not(target_arch = "wasm32"))]
            managed_runtime: Arc::new(OnceLock::new()),
        })
    }
}

pub fn build_reqwest_client(options: &ClientOptions) -> Result<Client> {
    let mut builder = Client::builder();

    if let Some(user_agent) = &options.user_agent {
        builder = builder.user_agent(user_agent);
    }
    let default_headers: Result<HeaderMap> = options
        .default_headers
        .iter()
        .map(|(k, v)| {
            Ok::<_, ReqwestBackendError>((
                HeaderName::from_bytes(k.as_bytes())
                    .map_err(|_| ReqwestBackendError::InvalidHeaderName(k.into()))?,
                HeaderValue::from_str(v)
                    .map_err(|_| ReqwestBackendError::InvalidHeaderValue(v.into()))?,
            ))
        })
        .collect();

    #[cfg(not(target_arch = "wasm32"))]
    {
        builder = build_non_wasm(builder, options);
    }

    builder
        .default_headers(default_headers?)
        .build()
        .map_err(ReqwestBackendError::Reqwest)
}

#[cfg(not(target_arch = "wasm32"))]
fn build_non_wasm(
    mut builder: reqwest::ClientBuilder,
    options: &ClientOptions,
) -> reqwest::ClientBuilder {
    if !options.use_default_proxy {
        builder = builder.no_proxy();
    }
    builder = builder
        .cookie_store(options.use_cookies)
        .redirect(if options.follow_redirects {
            reqwest::redirect::Policy::default()
        } else {
            reqwest::redirect::Policy::none()
        });
    #[cfg(any(
        feature = "default-tls",
        feature = "native-tls",
        feature = "rustls-tls-minimal",
    ))]
    {
        builder = builder.danger_accept_invalid_certs(options.ignore_certificate_errors);
    }

    if let Some(timeout) = options.request_timeout {
        builder = builder.timeout(timeout);
    }
    builder
}