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(status: StatusCode, headers: &HeaderMap) -> Option<Duration> {
123        if !is_retry_after_applicable_status(status) {
124            return None;
125        }
126        headers
127            .get(RETRY_AFTER)
128            .and_then(|value| value.to_str().ok())
129            .and_then(parse_retry_after_value)
130    }
131
132    /// Replaces response headers after response interceptors complete.
133    ///
134    /// # Parameters
135    /// - `headers`: New response headers.
136    ///
137    /// # Returns
138    /// Nothing.
139    pub(super) fn set_headers(&mut self, headers: HeaderMap) {
140        self.headers = headers;
141    }
142
143    /// Replaces final response URL after response interceptors complete.
144    ///
145    /// # Parameters
146    /// - `url`: New final response URL.
147    ///
148    /// # Returns
149    /// Nothing.
150    pub(super) fn set_url(&mut self, url: Url) {
151        self.url = url;
152    }
153
154    /// Returns the log sanitization policy snapshot for response diagnostics.
155    ///
156    /// # Returns
157    /// Borrowed policy snapshot.
158    pub(super) fn log_sanitize_policy(&self) -> &LogSanitizePolicy {
159        &self.log_sanitize_policy
160    }
161
162    /// Replaces the log sanitization policy snapshot.
163    ///
164    /// # Parameters
165    /// - `policy`: New policy snapshot.
166    ///
167    /// # Returns
168    /// Nothing.
169    pub(super) fn set_log_sanitize_policy(&mut self, policy: LogSanitizePolicy) {
170        self.log_sanitize_policy = policy;
171    }
172}
173
174impl fmt::Debug for HttpResponseMeta {
175    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176        let debugger = SanitizedDebugger::new(&self.log_sanitize_policy);
177        let url = debugger.url(&self.url);
178        formatter
179            .debug_struct("HttpResponseMeta")
180            .field("status", &self.status)
181            .field("headers", &debugger.headers(&self.headers))
182            .field("url", &url)
183            .field("method", &self.method)
184            .finish()
185    }
186}
187
188fn is_retry_after_applicable_status(status: StatusCode) -> bool {
189    status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
190}
191
192fn parse_retry_after_value(value: &str) -> Option<Duration> {
193    let trimmed = value.trim();
194    if trimmed.is_empty() {
195        return None;
196    }
197    if let Ok(seconds) = trimmed.parse::<u64>() {
198        return Some(Duration::from_secs(seconds));
199    }
200    let retry_at = parse_http_date(trimmed).ok()?;
201    let now = SystemTime::now();
202    Some(retry_at.duration_since(now).unwrap_or_else(|_| Duration::from_secs(0)))
203}