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}