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