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