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}