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}