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    pub status: StatusCode,
31    /// Response headers.
32    pub headers: HeaderMap,
33    /// Final resolved URL.
34    pub url: Url,
35    /// Originating request method.
36    pub 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 parsed `Retry-After` when this response status should honor it.
51    ///
52    /// Applicable statuses are `429` and `5xx`, and header value can be
53    /// `delta-seconds` or HTTP-date.
54    pub fn retry_after_hint(&self) -> Option<Duration> {
55        if !is_retry_after_applicable_status(self.status) {
56            return None;
57        }
58        self.headers
59            .get(RETRY_AFTER)
60            .and_then(|value| value.to_str().ok())
61            .and_then(parse_retry_after_value)
62    }
63}
64
65fn is_retry_after_applicable_status(status: StatusCode) -> bool {
66    status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
67}
68
69fn parse_retry_after_value(value: &str) -> Option<Duration> {
70    let trimmed = value.trim();
71    if trimmed.is_empty() {
72        return None;
73    }
74    if let Ok(seconds) = trimmed.parse::<u64>() {
75        return Some(Duration::from_secs(seconds));
76    }
77    let retry_at = parse_http_date(trimmed).ok()?;
78    let now = SystemTime::now();
79    Some(
80        retry_at
81            .duration_since(now)
82            .unwrap_or_else(|_| Duration::from_secs(0)),
83    )
84}