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}