Skip to main content

qubit_http/response/
http_response_meta.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//! Shared HTTP response metadata (status, headers, URL, request method).
11
12use std::fmt;
13use std::time::{
14    Duration,
15    SystemTime,
16};
17
18use http::header::RETRY_AFTER;
19use http::{
20    HeaderMap,
21    Method,
22    StatusCode,
23};
24use httpdate::parse_http_date;
25use url::Url;
26
27use crate::sanitize::SanitizedDebugger;
28use crate::LogSanitizePolicy;
29
30/// HTTP response metadata available before body buffering/stream consumption.
31#[derive(Clone)]
32pub struct HttpResponseMeta {
33    /// Response status code.
34    status: StatusCode,
35    /// Response headers.
36    headers: HeaderMap,
37    /// Final resolved URL.
38    url: Url,
39    /// Originating request method.
40    method: Method,
41    /// Sanitization policy snapshot used by standalone debug output.
42    log_sanitize_policy: LogSanitizePolicy,
43}
44
45impl HttpResponseMeta {
46    /// Creates response metadata from status/headers/url/method parts.
47    pub fn new(status: StatusCode, headers: HeaderMap, url: Url, method: Method) -> Self {
48        Self {
49            status,
50            headers,
51            url,
52            method,
53            log_sanitize_policy: LogSanitizePolicy::default(),
54        }
55    }
56
57    /// Attaches the log sanitization policy used for standalone debug output.
58    ///
59    /// # Parameters
60    /// - `policy`: Policy snapshot to apply when formatting this metadata.
61    ///
62    /// # Returns
63    /// Updated metadata.
64    pub fn with_log_sanitize_policy(mut self, policy: LogSanitizePolicy) -> Self {
65        self.log_sanitize_policy = policy;
66        self
67    }
68
69    /// Returns response status code.
70    ///
71    /// # Returns
72    /// Immutable response status.
73    #[inline]
74    pub fn status(&self) -> StatusCode {
75        self.status
76    }
77
78    /// Returns response headers.
79    ///
80    /// # Returns
81    /// Immutable response header map.
82    #[inline]
83    pub fn headers(&self) -> &HeaderMap {
84        &self.headers
85    }
86
87    /// Returns final response URL.
88    ///
89    /// # Returns
90    /// Immutable final response URL.
91    #[inline]
92    pub fn url(&self) -> &Url {
93        &self.url
94    }
95
96    /// Returns originating request method.
97    ///
98    /// # Returns
99    /// Immutable request method.
100    #[inline]
101    pub fn method(&self) -> &Method {
102        &self.method
103    }
104
105    /// Returns parsed `Retry-After` when this response status should honor it.
106    ///
107    /// Applicable statuses are `429` and `5xx`, and header value can be
108    /// `delta-seconds` or HTTP-date.
109    pub fn retry_after_hint(&self) -> Option<Duration> {
110        Self::retry_after_hint_from_parts(self.status, &self.headers)
111    }
112
113    /// Returns parsed `Retry-After` for explicit status/header parts.
114    ///
115    /// # Parameters
116    /// - `status`: Response status code to inspect.
117    /// - `headers`: Response headers that may contain `Retry-After`.
118    ///
119    /// # Returns
120    /// `Some(Duration)` when status and header value are applicable; otherwise
121    /// `None`.
122    pub(super) fn retry_after_hint_from_parts(
123        status: StatusCode,
124        headers: &HeaderMap,
125    ) -> Option<Duration> {
126        if !is_retry_after_applicable_status(status) {
127            return None;
128        }
129        headers
130            .get(RETRY_AFTER)
131            .and_then(|value| value.to_str().ok())
132            .and_then(parse_retry_after_value)
133    }
134
135    /// Replaces response headers after response interceptors complete.
136    ///
137    /// # Parameters
138    /// - `headers`: New response headers.
139    ///
140    /// # Returns
141    /// Nothing.
142    pub(super) fn set_headers(&mut self, headers: HeaderMap) {
143        self.headers = headers;
144    }
145
146    /// Replaces final response URL after response interceptors complete.
147    ///
148    /// # Parameters
149    /// - `url`: New final response URL.
150    ///
151    /// # Returns
152    /// Nothing.
153    pub(super) fn set_url(&mut self, url: Url) {
154        self.url = url;
155    }
156
157    /// Returns the log sanitization policy snapshot for response diagnostics.
158    ///
159    /// # Returns
160    /// Borrowed policy snapshot.
161    pub(super) fn log_sanitize_policy(&self) -> &LogSanitizePolicy {
162        &self.log_sanitize_policy
163    }
164
165    /// Replaces the log sanitization policy snapshot.
166    ///
167    /// # Parameters
168    /// - `policy`: New policy snapshot.
169    ///
170    /// # Returns
171    /// Nothing.
172    pub(super) fn set_log_sanitize_policy(&mut self, policy: LogSanitizePolicy) {
173        self.log_sanitize_policy = policy;
174    }
175}
176
177impl fmt::Debug for HttpResponseMeta {
178    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179        let debugger = SanitizedDebugger::new(&self.log_sanitize_policy);
180        let url = debugger.url(&self.url);
181        formatter
182            .debug_struct("HttpResponseMeta")
183            .field("status", &self.status)
184            .field("headers", &debugger.headers(&self.headers))
185            .field("url", &url)
186            .field("method", &self.method)
187            .finish()
188    }
189}
190
191fn is_retry_after_applicable_status(status: StatusCode) -> bool {
192    status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
193}
194
195fn parse_retry_after_value(value: &str) -> Option<Duration> {
196    let trimmed = value.trim();
197    if trimmed.is_empty() {
198        return None;
199    }
200    if let Ok(seconds) = trimmed.parse::<u64>() {
201        return Some(Duration::from_secs(seconds));
202    }
203    let retry_at = parse_http_date(trimmed).ok()?;
204    let now = SystemTime::now();
205    Some(
206        retry_at
207            .duration_since(now)
208            .unwrap_or_else(|_| Duration::from_secs(0)),
209    )
210}