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}