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::time::{
13    Duration,
14    SystemTime,
15};
16
17use http::header::RETRY_AFTER;
18use http::{
19    HeaderMap,
20    Method,
21    StatusCode,
22};
23use httpdate::parse_http_date;
24use url::Url;
25
26/// HTTP response metadata available before body buffering/stream consumption.
27#[derive(Debug, Clone)]
28pub struct HttpResponseMeta {
29    /// Response status code.
30    status: StatusCode,
31    /// Response headers.
32    headers: HeaderMap,
33    /// Final resolved URL.
34    url: Url,
35    /// Originating request method.
36    method: Method,
37}
38
39impl HttpResponseMeta {
40    /// Creates response metadata from status/headers/url/method parts.
41    pub fn new(status: StatusCode, headers: HeaderMap, url: Url, method: Method) -> Self {
42        Self {
43            status,
44            headers,
45            url,
46            method,
47        }
48    }
49
50    /// Returns response status code.
51    ///
52    /// # Returns
53    /// Immutable response status.
54    #[inline]
55    pub fn status(&self) -> StatusCode {
56        self.status
57    }
58
59    /// Returns response headers.
60    ///
61    /// # Returns
62    /// Immutable response header map.
63    #[inline]
64    pub fn headers(&self) -> &HeaderMap {
65        &self.headers
66    }
67
68    /// Returns final response URL.
69    ///
70    /// # Returns
71    /// Immutable final response URL.
72    #[inline]
73    pub fn url(&self) -> &Url {
74        &self.url
75    }
76
77    /// Returns originating request method.
78    ///
79    /// # Returns
80    /// Immutable request method.
81    #[inline]
82    pub fn method(&self) -> &Method {
83        &self.method
84    }
85
86    /// Returns parsed `Retry-After` when this response status should honor it.
87    ///
88    /// Applicable statuses are `429` and `5xx`, and header value can be
89    /// `delta-seconds` or HTTP-date.
90    pub fn retry_after_hint(&self) -> Option<Duration> {
91        Self::retry_after_hint_from_parts(self.status, &self.headers)
92    }
93
94    /// Returns parsed `Retry-After` for explicit status/header parts.
95    ///
96    /// # Parameters
97    /// - `status`: Response status code to inspect.
98    /// - `headers`: Response headers that may contain `Retry-After`.
99    ///
100    /// # Returns
101    /// `Some(Duration)` when status and header value are applicable; otherwise
102    /// `None`.
103    pub(super) fn retry_after_hint_from_parts(
104        status: StatusCode,
105        headers: &HeaderMap,
106    ) -> Option<Duration> {
107        if !is_retry_after_applicable_status(status) {
108            return None;
109        }
110        headers
111            .get(RETRY_AFTER)
112            .and_then(|value| value.to_str().ok())
113            .and_then(parse_retry_after_value)
114    }
115
116    /// Replaces response headers after response interceptors complete.
117    ///
118    /// # Parameters
119    /// - `headers`: New response headers.
120    ///
121    /// # Returns
122    /// Nothing.
123    pub(super) fn set_headers(&mut self, headers: HeaderMap) {
124        self.headers = headers;
125    }
126
127    /// Replaces final response URL after response interceptors complete.
128    ///
129    /// # Parameters
130    /// - `url`: New final response URL.
131    ///
132    /// # Returns
133    /// Nothing.
134    pub(super) fn set_url(&mut self, url: Url) {
135        self.url = url;
136    }
137}
138
139fn is_retry_after_applicable_status(status: StatusCode) -> bool {
140    status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
141}
142
143fn parse_retry_after_value(value: &str) -> Option<Duration> {
144    let trimmed = value.trim();
145    if trimmed.is_empty() {
146        return None;
147    }
148    if let Ok(seconds) = trimmed.parse::<u64>() {
149        return Some(Duration::from_secs(seconds));
150    }
151    let retry_at = parse_http_date(trimmed).ok()?;
152    let now = SystemTime::now();
153    Some(
154        retry_at
155            .duration_since(now)
156            .unwrap_or_else(|_| Duration::from_secs(0)),
157    )
158}