qubit-http 0.5.0

General-purpose HTTP infrastructure for Rust with unified client semantics, secure logging, and built-in SSE decoding
Documentation
/*******************************************************************************
 *
 *    Copyright (c) 2025 - 2026 Haixing Hu.
 *
 *    SPDX-License-Identifier: Apache-2.0
 *
 *    Licensed under the Apache License, Version 2.0.
 *
 ******************************************************************************/
#![allow(clippy::result_large_err)]
// Keep `HttpError` rich (method/url/status/source) for diagnostics and retry decisions
// across the crate's public APIs.

//! # Qubit HTTP
//!
//! A general-purpose HTTP infrastructure module for Rust services.
//!
//! This crate provides:
//! - Unified HTTP client options and factory abstractions
//! - Loading those options from [`qubit_config::ConfigReader`] (`from_config` / factory `create_from_config`)
//! - Consistent request/response/stream APIs
//! - Secure and configurable logging with sensitive header masking
//! - Built-in SSE decoding utilities in [`sse`]
//! - Unified error model and retry hints
//!

mod client;
pub mod constants;
mod error;
mod options;
mod request;
mod response;
pub mod sse;

#[cfg(coverage)]
#[doc(hidden)]
pub mod coverage_support {
    //! Coverage-only hooks for defensive branches not reachable through normal APIs.

    use std::time::{Duration, Instant};

    use http::{HeaderMap, HeaderValue, Method, StatusCode};
    use qubit_retry::{AttemptFailure, RetryContext, RetryError, RetryErrorReason};
    use url::Url;

    use crate::error::backend_error_mapper;
    use crate::error::ReqwestErrorPhase;
    use crate::{HttpError, HttpErrorKind, HttpResponse};

    /// Exercises timeout classifier branches that require synthetic reqwest metadata.
    ///
    /// # Returns
    /// Kinds selected for connect, send, read, and unknown timeout phases.
    pub fn classify_timeout_kinds() -> Vec<HttpErrorKind> {
        vec![
            backend_error_mapper::coverage_classify_timeout_kind(
                true,
                Some(ReqwestErrorPhase::Send),
            ),
            backend_error_mapper::coverage_classify_timeout_kind(
                false,
                Some(ReqwestErrorPhase::Send),
            ),
            backend_error_mapper::coverage_classify_timeout_kind(
                false,
                Some(ReqwestErrorPhase::Read),
            ),
            backend_error_mapper::coverage_classify_timeout_kind(false, None),
        ]
    }

    /// Exercises non-timeout reqwest classifier branches with synthetic metadata.
    ///
    /// # Returns
    /// Kinds selected for decode, status, invalid URL, and fallback categories.
    pub fn classify_backend_error_kinds() -> Vec<HttpErrorKind> {
        vec![
            backend_error_mapper::coverage_classify_reqwest_error_kind(
                false,
                true,
                false,
                false,
                false,
                None,
                HttpErrorKind::Transport,
            ),
            backend_error_mapper::coverage_classify_reqwest_error_kind(
                false,
                false,
                true,
                false,
                false,
                None,
                HttpErrorKind::Transport,
            ),
            backend_error_mapper::coverage_classify_reqwest_error_kind(
                false,
                false,
                false,
                true,
                false,
                None,
                HttpErrorKind::Transport,
            ),
            backend_error_mapper::coverage_classify_reqwest_error_kind(
                false,
                false,
                false,
                false,
                false,
                None,
                HttpErrorKind::Transport,
            ),
        ]
    }

    /// Exercises HTTP retry mapping branches that are defensive in normal async use.
    ///
    /// # Returns
    /// Diagnostic messages from each mapped retry terminal path.
    pub fn exercise_http_retry_mapping_paths() -> Vec<String> {
        let started_at = Instant::now();
        let context = RetryContext::new(2, 2);
        let attempts_exceeded = RetryError::<HttpError>::coverage_new(
            RetryErrorReason::AttemptsExceeded,
            None,
            context,
        );
        let aborted =
            RetryError::<HttpError>::coverage_new(RetryErrorReason::Aborted, None, context);
        let unsupported = RetryError::<HttpError>::coverage_new(
            RetryErrorReason::UnsupportedOperation,
            None,
            context,
        );
        let max_elapsed = RetryError::<HttpError>::coverage_new(
            RetryErrorReason::MaxTotalElapsedExceeded,
            None,
            context,
        );
        let max_elapsed_unbounded = RetryError::<HttpError>::coverage_new(
            RetryErrorReason::MaxTotalElapsedExceeded,
            None,
            context,
        );

        vec![
            crate::HttpClient::coverage_map_retry_error(
                attempts_exceeded,
                started_at,
                Some(Duration::from_millis(1)),
                2,
            )
            .message,
            crate::HttpClient::coverage_map_retry_error(
                aborted,
                started_at,
                Some(Duration::from_millis(1)),
                2,
            )
            .message,
            crate::HttpClient::coverage_map_retry_error(
                unsupported,
                started_at,
                Some(Duration::from_millis(1)),
                2,
            )
            .message,
            crate::HttpClient::coverage_map_retry_error(
                max_elapsed,
                started_at,
                Some(Duration::from_millis(1)),
                2,
            )
            .message,
            crate::HttpClient::coverage_map_retry_error(max_elapsed_unbounded, started_at, None, 2)
                .message,
        ]
    }

    /// Exercises the retry failure decision branch for non-application failures.
    ///
    /// # Returns
    /// Debug representation of the selected failure decision.
    pub fn exercise_retry_failure_decision_path() -> String {
        let retry_options = crate::HttpRetryOptions::default()
            .to_executor_options()
            .expect("default retry options should build");
        let decision = crate::HttpClient::coverage_retry_failure_decision(
            &AttemptFailure::Timeout,
            &RetryContext::new(1, 2),
            &crate::HttpRetryOptions::default(),
            &retry_options,
        );
        format!("{decision:?}")
    }

    /// Validates an SSE response with a deliberately non-UTF8 Content-Type header.
    ///
    /// # Returns
    /// The resulting error kind and message.
    pub fn validate_non_utf8_sse_content_type() -> (HttpErrorKind, String) {
        let mut headers = HeaderMap::new();
        headers.insert(
            http::header::CONTENT_TYPE,
            HeaderValue::from_bytes(b"\xFF").expect("opaque header bytes should be accepted"),
        );
        let response = HttpResponse::new(
            StatusCode::OK,
            headers,
            bytes::Bytes::new(),
            Url::parse("https://example.com/sse").expect("coverage URL should parse"),
            Method::GET,
        );
        let error = crate::sse::coverage_validate_sse_response_content_type(&response)
            .expect_err("non-UTF8 content type should be rejected");
        (error.kind, error.message)
    }

    /// Exercises response preview branches that require internal response state.
    ///
    /// # Returns
    /// Preview strings and cancellation kind diagnostics.
    pub async fn exercise_response_preview_paths() -> Vec<String> {
        crate::response::coverage_exercise_response_preview_paths().await
    }

    /// Exercises request cache branches that are not directly exposed publicly.
    ///
    /// # Returns
    /// Diagnostic strings from the resolved URL and header cache paths.
    pub async fn exercise_request_cache_paths() -> Vec<String> {
        crate::request::coverage_exercise_request_cache_paths().await
    }

    /// Exercises internal threshold-oriented defensive paths.
    ///
    /// # Returns
    /// Diagnostic strings from factory, logger, client, and mapper paths.
    pub async fn exercise_threshold_paths() -> Vec<String> {
        let mut diagnostics = crate::client::coverage_exercise_factory_paths();
        diagnostics.push(crate::client::coverage_exercise_request_log_url_fallback());
        diagnostics.push(format!(
            "{:?}",
            crate::HttpClient::coverage_prepare_cancelled_error().await
        ));
        diagnostics.extend(crate::options::coverage_exercise_http_client_option_paths());
        diagnostics.extend(crate::options::coverage_exercise_config_error_paths());
        diagnostics.push(crate::options::coverage_exercise_retry_option_paths());
        diagnostics
    }
}

pub use client::http_logger::HttpLogger;
pub use client::HttpClient;
pub use client::HttpClientFactory;
pub use constants::DEFAULT_SENSITIVE_HEADER_NAMES;
pub use error::{HttpError, HttpErrorKind, HttpResult, RetryHint};
pub use options::{
    HttpClientOptions, HttpConfigError, HttpConfigErrorKind, HttpLoggingOptions,
    HttpRetryMethodPolicy, HttpRetryOptions, HttpTimeoutOptions, ProxyOptions, ProxyType,
    SensitiveHttpHeaders,
};
pub use qubit_retry::{RetryDelay, RetryJitter, RetryOptions};
pub use request::{
    AsyncHttpHeaderInjector, HttpHeaderInjector, HttpRequest, HttpRequestBody,
    HttpRequestBodyByteStream, HttpRequestBuilder, HttpRequestInterceptor, HttpRequestInterceptors,
    HttpRequestRetryOverride, HttpRequestStreamingBody,
};
pub use response::{
    HttpByteStream, HttpResponse, HttpResponseInterceptor, HttpResponseInterceptors,
    HttpResponseMeta,
};
pub use tokio_util::sync::CancellationToken;