Skip to main content

qubit_http/response/
http_response_meta.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! Shared HTTP response metadata (status, headers, URL, request method).
10
11use std::time::{Duration, SystemTime};
12
13use http::header::RETRY_AFTER;
14use http::{HeaderMap, Method, StatusCode};
15use httpdate::parse_http_date;
16use url::Url;
17
18/// HTTP response metadata available before body buffering/stream consumption.
19#[derive(Debug, Clone)]
20pub struct HttpResponseMeta {
21    /// Response status code.
22    pub status: StatusCode,
23    /// Response headers.
24    pub headers: HeaderMap,
25    /// Final resolved URL.
26    pub url: Url,
27    /// Originating request method.
28    pub method: Method,
29}
30
31impl HttpResponseMeta {
32    /// Creates response metadata from status/headers/url/method parts.
33    pub fn new(status: StatusCode, headers: HeaderMap, url: Url, method: Method) -> Self {
34        Self {
35            status,
36            headers,
37            url,
38            method,
39        }
40    }
41
42    /// Returns parsed `Retry-After` when this response status should honor it.
43    ///
44    /// Applicable statuses are `429` and `5xx`, and header value can be
45    /// `delta-seconds` or HTTP-date.
46    pub fn retry_after_hint(&self) -> Option<Duration> {
47        if !is_retry_after_applicable_status(self.status) {
48            return None;
49        }
50        self.headers
51            .get(RETRY_AFTER)
52            .and_then(|value| value.to_str().ok())
53            .and_then(parse_retry_after_value)
54    }
55}
56
57fn is_retry_after_applicable_status(status: StatusCode) -> bool {
58    status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
59}
60
61fn parse_retry_after_value(value: &str) -> Option<Duration> {
62    let trimmed = value.trim();
63    if trimmed.is_empty() {
64        return None;
65    }
66    if let Ok(seconds) = trimmed.parse::<u64>() {
67        return Some(Duration::from_secs(seconds));
68    }
69    let retry_at = parse_http_date(trimmed).ok()?;
70    let now = SystemTime::now();
71    Some(
72        retry_at
73            .duration_since(now)
74            .unwrap_or_else(|_| Duration::from_secs(0)),
75    )
76}