Skip to main content

qubit_http/
lib.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10#![allow(clippy::result_large_err)]
11// Keep `HttpError` rich (method/url/status/source) for diagnostics and retry decisions
12// across the crate's public APIs.
13
14//! # Qubit HTTP
15//!
16//! A general-purpose HTTP infrastructure module for Rust services.
17//!
18//! This crate provides:
19//! - Unified HTTP client options and factory abstractions
20//! - Loading those options from [`qubit_config::ConfigReader`] (`from_config` / factory `create_from_config`)
21//! - Consistent request/response/stream APIs
22//! - Secure and configurable logging with sensitive header masking
23//! - Built-in SSE decoding utilities in [`sse`]
24//! - Unified error model and retry hints
25//!
26
27mod client;
28pub mod constants;
29mod error;
30mod options;
31mod request;
32mod response;
33pub mod sse;
34
35#[cfg(coverage)]
36#[doc(hidden)]
37pub mod coverage_support {
38    //! Coverage-only hooks for defensive branches not reachable through normal APIs.
39
40    use std::time::{Duration, Instant};
41
42    use http::{HeaderMap, HeaderValue, Method, StatusCode};
43    use qubit_retry::{AttemptFailure, RetryContext, RetryError, RetryErrorReason};
44    use url::Url;
45
46    use crate::error::backend_error_mapper;
47    use crate::error::ReqwestErrorPhase;
48    use crate::{HttpError, HttpErrorKind, HttpResponse};
49
50    /// Exercises timeout classifier branches that require synthetic reqwest metadata.
51    ///
52    /// # Returns
53    /// Kinds selected for connect, send, read, and unknown timeout phases.
54    pub fn classify_timeout_kinds() -> Vec<HttpErrorKind> {
55        vec![
56            backend_error_mapper::coverage_classify_timeout_kind(
57                true,
58                Some(ReqwestErrorPhase::Send),
59            ),
60            backend_error_mapper::coverage_classify_timeout_kind(
61                false,
62                Some(ReqwestErrorPhase::Send),
63            ),
64            backend_error_mapper::coverage_classify_timeout_kind(
65                false,
66                Some(ReqwestErrorPhase::Read),
67            ),
68            backend_error_mapper::coverage_classify_timeout_kind(false, None),
69        ]
70    }
71
72    /// Exercises non-timeout reqwest classifier branches with synthetic metadata.
73    ///
74    /// # Returns
75    /// Kinds selected for decode, status, invalid URL, and fallback categories.
76    pub fn classify_backend_error_kinds() -> Vec<HttpErrorKind> {
77        vec![
78            backend_error_mapper::coverage_classify_reqwest_error_kind(
79                false,
80                true,
81                false,
82                false,
83                false,
84                None,
85                HttpErrorKind::Transport,
86            ),
87            backend_error_mapper::coverage_classify_reqwest_error_kind(
88                false,
89                false,
90                true,
91                false,
92                false,
93                None,
94                HttpErrorKind::Transport,
95            ),
96            backend_error_mapper::coverage_classify_reqwest_error_kind(
97                false,
98                false,
99                false,
100                true,
101                false,
102                None,
103                HttpErrorKind::Transport,
104            ),
105            backend_error_mapper::coverage_classify_reqwest_error_kind(
106                false,
107                false,
108                false,
109                false,
110                false,
111                None,
112                HttpErrorKind::Transport,
113            ),
114        ]
115    }
116
117    /// Exercises HTTP retry mapping branches that are defensive in normal async use.
118    ///
119    /// # Returns
120    /// Diagnostic messages from each mapped retry terminal path.
121    pub fn exercise_http_retry_mapping_paths() -> Vec<String> {
122        let started_at = Instant::now();
123        let context = RetryContext::new(2, 2);
124        let attempts_exceeded = RetryError::<HttpError>::coverage_new(
125            RetryErrorReason::AttemptsExceeded,
126            None,
127            context,
128        );
129        let aborted =
130            RetryError::<HttpError>::coverage_new(RetryErrorReason::Aborted, None, context);
131        let unsupported = RetryError::<HttpError>::coverage_new(
132            RetryErrorReason::UnsupportedOperation,
133            None,
134            context,
135        );
136        let max_elapsed = RetryError::<HttpError>::coverage_new(
137            RetryErrorReason::MaxTotalElapsedExceeded,
138            None,
139            context,
140        );
141        let max_elapsed_unbounded = RetryError::<HttpError>::coverage_new(
142            RetryErrorReason::MaxTotalElapsedExceeded,
143            None,
144            context,
145        );
146
147        vec![
148            crate::HttpClient::coverage_map_retry_error(
149                attempts_exceeded,
150                started_at,
151                Some(Duration::from_millis(1)),
152                2,
153            )
154            .message,
155            crate::HttpClient::coverage_map_retry_error(
156                aborted,
157                started_at,
158                Some(Duration::from_millis(1)),
159                2,
160            )
161            .message,
162            crate::HttpClient::coverage_map_retry_error(
163                unsupported,
164                started_at,
165                Some(Duration::from_millis(1)),
166                2,
167            )
168            .message,
169            crate::HttpClient::coverage_map_retry_error(
170                max_elapsed,
171                started_at,
172                Some(Duration::from_millis(1)),
173                2,
174            )
175            .message,
176            crate::HttpClient::coverage_map_retry_error(max_elapsed_unbounded, started_at, None, 2)
177                .message,
178        ]
179    }
180
181    /// Exercises the retry failure decision branch for non-application failures.
182    ///
183    /// # Returns
184    /// Debug representation of the selected failure decision.
185    pub fn exercise_retry_failure_decision_path() -> String {
186        let retry_options = crate::HttpRetryOptions::default()
187            .to_executor_options()
188            .expect("default retry options should build");
189        let decision = crate::HttpClient::coverage_retry_failure_decision(
190            &AttemptFailure::Timeout,
191            &RetryContext::new(1, 2),
192            &crate::HttpRetryOptions::default(),
193            &retry_options,
194        );
195        format!("{decision:?}")
196    }
197
198    /// Validates an SSE response with a deliberately non-UTF8 Content-Type header.
199    ///
200    /// # Returns
201    /// The resulting error kind and message.
202    pub fn validate_non_utf8_sse_content_type() -> (HttpErrorKind, String) {
203        let mut headers = HeaderMap::new();
204        headers.insert(
205            http::header::CONTENT_TYPE,
206            HeaderValue::from_bytes(b"\xFF").expect("opaque header bytes should be accepted"),
207        );
208        let response = HttpResponse::new(
209            StatusCode::OK,
210            headers,
211            bytes::Bytes::new(),
212            Url::parse("https://example.com/sse").expect("coverage URL should parse"),
213            Method::GET,
214        );
215        let error = crate::sse::coverage_validate_sse_response_content_type(&response)
216            .expect_err("non-UTF8 content type should be rejected");
217        (error.kind, error.message)
218    }
219
220    /// Exercises response preview branches that require internal response state.
221    ///
222    /// # Returns
223    /// Preview strings and cancellation kind diagnostics.
224    pub async fn exercise_response_preview_paths() -> Vec<String> {
225        crate::response::coverage_exercise_response_preview_paths().await
226    }
227
228    /// Exercises request cache branches that are not directly exposed publicly.
229    ///
230    /// # Returns
231    /// Diagnostic strings from the resolved URL and header cache paths.
232    pub async fn exercise_request_cache_paths() -> Vec<String> {
233        crate::request::coverage_exercise_request_cache_paths().await
234    }
235
236    /// Exercises internal threshold-oriented defensive paths.
237    ///
238    /// # Returns
239    /// Diagnostic strings from factory, logger, client, and mapper paths.
240    pub async fn exercise_threshold_paths() -> Vec<String> {
241        let mut diagnostics = crate::client::coverage_exercise_factory_paths();
242        diagnostics.push(crate::client::coverage_exercise_request_log_url_fallback());
243        diagnostics.push(format!(
244            "{:?}",
245            crate::HttpClient::coverage_prepare_cancelled_error().await
246        ));
247        diagnostics.extend(crate::options::coverage_exercise_http_client_option_paths());
248        diagnostics.extend(crate::options::coverage_exercise_config_error_paths());
249        diagnostics.push(crate::options::coverage_exercise_retry_option_paths());
250        diagnostics
251    }
252}
253
254pub use client::http_logger::HttpLogger;
255pub use client::HttpClient;
256pub use client::HttpClientFactory;
257pub use constants::DEFAULT_SENSITIVE_HEADER_NAMES;
258pub use error::{HttpError, HttpErrorKind, HttpResult, RetryHint};
259pub use options::{
260    HttpClientOptions, HttpConfigError, HttpConfigErrorKind, HttpLoggingOptions,
261    HttpRetryMethodPolicy, HttpRetryOptions, HttpTimeoutOptions, ProxyOptions, ProxyType,
262    SensitiveHttpHeaders,
263};
264pub use qubit_retry::{RetryDelay, RetryJitter, RetryOptions};
265pub use request::{
266    AsyncHttpHeaderInjector, HttpHeaderInjector, HttpRequest, HttpRequestBody,
267    HttpRequestBodyByteStream, HttpRequestBuilder, HttpRequestInterceptor, HttpRequestInterceptors,
268    HttpRequestRetryOverride, HttpRequestStreamingBody,
269};
270pub use response::{
271    HttpByteStream, HttpResponse, HttpResponseInterceptor, HttpResponseInterceptors,
272    HttpResponseMeta,
273};
274pub use tokio_util::sync::CancellationToken;