qubit_http/request/http_request_interceptors.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026.
4 * Haixing Hu, Qubit Co. Ltd.
5 *
6 * All rights reserved.
7 *
8 ******************************************************************************/
9//! Request interceptor abstraction for outgoing HTTP requests.
10
11use qubit_function::{ArcMutatingFunction, MutatingFunction};
12use url::Url;
13
14use super::http_request::HttpRequest;
15use crate::HttpResult;
16
17/// Request interceptor function used to mutate an outbound [`HttpRequest`]
18/// before URL resolution, header merge, and network I/O.
19///
20/// Returning `Err` short-circuits execution for the current attempt.
21pub type HttpRequestInterceptor = ArcMutatingFunction<HttpRequest, HttpResult<()>>;
22
23/// Ordered request interceptor list with unified application behavior.
24#[derive(Debug, Clone, Default)]
25pub struct HttpRequestInterceptors {
26 interceptors: Vec<HttpRequestInterceptor>,
27}
28
29impl HttpRequestInterceptors {
30 /// Creates an empty request interceptor list.
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 /// Appends one request interceptor.
36 pub fn push(&mut self, interceptor: HttpRequestInterceptor) {
37 self.interceptors.push(interceptor);
38 }
39
40 /// Removes all request interceptors.
41 pub fn clear(&mut self) {
42 self.interceptors.clear();
43 }
44
45 /// Applies request interceptors in insertion order.
46 ///
47 /// # Parameters
48 /// - `request`: Request snapshot to mutate before URL resolution and send.
49 ///
50 /// # Returns
51 /// `Ok(())` when all interceptors succeed.
52 ///
53 /// # Errors
54 /// Returns the first interceptor error and enriches it with method/URL
55 /// context when missing.
56 pub fn apply(&self, request: &mut HttpRequest) -> HttpResult<()> {
57 for interceptor in &self.interceptors {
58 interceptor.apply(request).map_err(|error| {
59 let mut mapped = error;
60 if mapped.method.is_none() {
61 mapped = mapped.with_method(request.method());
62 }
63 if mapped.url.is_none() {
64 // Prefer the fully resolved request URL so builder query
65 // params are visible in interceptor errors; fallback to the
66 // raw URL to preserve behavior when resolution fails.
67 if let Ok(resolved_url) = request.resolved_url_with_query() {
68 mapped = mapped.with_url(&resolved_url);
69 } else if let Ok(parsed_url) = Url::parse(request.path()) {
70 mapped = mapped.with_url(&parsed_url);
71 }
72 }
73 mapped
74 })?;
75 }
76 Ok(())
77 }
78}