Skip to main content

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