Skip to main content

qubit_http/response/
http_response_interceptor_context.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//! Response interceptor context with controlled metadata mutation.
11
12use std::fmt;
13use std::time::Duration;
14
15use http::{
16    HeaderMap,
17    Method,
18    StatusCode,
19};
20use url::Url;
21
22use super::HttpResponseMeta;
23use crate::sanitize::SanitizedDebugger;
24use crate::LogSanitizePolicy;
25
26/// Metadata view passed to response interceptors.
27///
28/// The HTTP status and originating method are immutable so interceptors cannot
29/// invalidate the `HttpClient::execute` success-status contract after the
30/// client has already accepted the response. Interceptors may still mutate
31/// response headers and the final response URL.
32#[derive(Clone)]
33pub struct HttpResponseInterceptorContext {
34    /// Response status code captured before interceptor execution.
35    status: StatusCode,
36    /// Mutable response headers visible to later interceptors and callers.
37    headers: HeaderMap,
38    /// Mutable final response URL visible to later interceptors and callers.
39    url: Url,
40    /// Originating request method captured before interceptor execution.
41    method: Method,
42    /// Sanitization policy snapshot used by standalone debug output.
43    log_sanitize_policy: LogSanitizePolicy,
44}
45
46impl HttpResponseInterceptorContext {
47    /// Creates a response interceptor context from explicit metadata parts.
48    ///
49    /// # Parameters
50    /// - `status`: Response status code. It is immutable after construction.
51    /// - `headers`: Response headers that interceptors may mutate.
52    /// - `url`: Final response URL that interceptors may replace.
53    /// - `method`: Originating request method. It is immutable after construction.
54    ///
55    /// # Returns
56    /// New interceptor context.
57    pub fn new(status: StatusCode, headers: HeaderMap, url: Url, method: Method) -> Self {
58        Self {
59            status,
60            headers,
61            url,
62            method,
63            log_sanitize_policy: LogSanitizePolicy::default(),
64        }
65    }
66
67    /// Attaches the log sanitization policy used for standalone debug output.
68    ///
69    /// # Parameters
70    /// - `policy`: Policy snapshot to apply when formatting this context.
71    ///
72    /// # Returns
73    /// Updated context.
74    pub fn with_log_sanitize_policy(mut self, policy: LogSanitizePolicy) -> Self {
75        self.log_sanitize_policy = policy;
76        self
77    }
78
79    /// Copies response metadata into a mutable interceptor context.
80    ///
81    /// # Parameters
82    /// - `meta`: Source response metadata.
83    ///
84    /// # Returns
85    /// New context with cloned headers, URL, and method.
86    pub fn from_meta(meta: &HttpResponseMeta) -> Self {
87        Self::new(
88            meta.status(),
89            meta.headers().clone(),
90            meta.url().clone(),
91            meta.method().clone(),
92        )
93        .with_log_sanitize_policy(meta.log_sanitize_policy().clone())
94    }
95
96    /// Returns response status code.
97    ///
98    /// # Returns
99    /// Immutable status accepted by `HttpClient::execute`.
100    #[inline]
101    pub fn status(&self) -> StatusCode {
102        self.status
103    }
104
105    /// Returns response headers.
106    ///
107    /// # Returns
108    /// Immutable header map view.
109    #[inline]
110    pub fn headers(&self) -> &HeaderMap {
111        &self.headers
112    }
113
114    /// Returns mutable response headers.
115    ///
116    /// # Returns
117    /// Mutable header map applied back to [`HttpResponseMeta`] after all
118    /// response interceptors succeed.
119    #[inline]
120    pub fn headers_mut(&mut self) -> &mut HeaderMap {
121        &mut self.headers
122    }
123
124    /// Returns final response URL.
125    ///
126    /// # Returns
127    /// Immutable response URL view.
128    #[inline]
129    pub fn url(&self) -> &Url {
130        &self.url
131    }
132
133    /// Replaces final response URL.
134    ///
135    /// # Parameters
136    /// - `url`: New final response URL.
137    ///
138    /// # Returns
139    /// `self` for method chaining.
140    #[inline]
141    pub fn set_url(&mut self, url: Url) -> &mut Self {
142        self.url = url;
143        self
144    }
145
146    /// Returns originating request method.
147    ///
148    /// # Returns
149    /// Immutable request method.
150    #[inline]
151    pub fn method(&self) -> &Method {
152        &self.method
153    }
154
155    /// Returns parsed `Retry-After` when status and headers provide one.
156    ///
157    /// # Returns
158    /// `Some(Duration)` for retryable status codes with valid `Retry-After`;
159    /// otherwise `None`.
160    #[inline]
161    pub fn retry_after_hint(&self) -> Option<Duration> {
162        HttpResponseMeta::retry_after_hint_from_parts(self.status, &self.headers)
163    }
164
165    /// Applies mutable context fields back into response metadata.
166    ///
167    /// # Parameters
168    /// - `meta`: Response metadata to update.
169    ///
170    /// # Returns
171    /// Nothing. Status and method are intentionally not copied back.
172    pub(super) fn apply_to_meta(self, meta: &mut HttpResponseMeta) {
173        meta.set_headers(self.headers);
174        meta.set_url(self.url);
175        meta.set_log_sanitize_policy(self.log_sanitize_policy);
176    }
177}
178
179impl fmt::Debug for HttpResponseInterceptorContext {
180    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
181        let debugger = SanitizedDebugger::new(&self.log_sanitize_policy);
182        let url = debugger.url(&self.url);
183        formatter
184            .debug_struct("HttpResponseInterceptorContext")
185            .field("status", &self.status)
186            .field("headers", &debugger.headers(&self.headers))
187            .field("url", &url)
188            .field("method", &self.method)
189            .finish()
190    }
191}