Skip to main content

better_fetch/
hooks.rs

1//! Lifecycle hooks for requests and responses.
2//!
3//! [`Hooks::on_request`] and [`Hooks::on_response`] return [`Result`]. To abort the client
4//! pipeline intentionally, return `Err(Error::hook("reason"))`. Other [`Error`] variants
5//! (`Transport`, `Http`, …) are valid when a hook needs to surface a specific failure.
6//! [`Hooks::on_success`], [`Hooks::on_error`], and [`Hooks::on_retry`] cannot return errors.
7
8use std::future::Future;
9use std::pin::Pin;
10use std::sync::Arc;
11
12use bytes::Bytes;
13use http::{HeaderMap, Method};
14use url::Url;
15
16use crate::error::Error;
17use crate::response::Response;
18use crate::Result;
19
20/// Context for an outgoing request.
21#[derive(Debug, Clone)]
22pub struct RequestContext {
23    pub url: Url,
24    pub method: Method,
25    pub headers: HeaderMap,
26    pub body: Option<Bytes>,
27    /// Number of times this request has already been retried (`0` on the first HTTP attempt).
28    ///
29    /// Matches JS [`retryAttempt`](https://better-fetch.vercel.app/docs/fetch-options).
30    pub retry_attempt: u32,
31}
32
33/// Context after a response is received.
34#[derive(Debug, Clone)]
35pub struct ResponseContext {
36    pub request: RequestContext,
37    pub response: Response,
38}
39
40/// Context after a successful HTTP response (2xx).
41#[derive(Debug, Clone)]
42pub struct SuccessContext {
43    pub request: RequestContext,
44    pub response: Response,
45}
46
47/// Context when an error occurs.
48#[derive(Debug, Clone)]
49pub struct ErrorContext {
50    pub request: RequestContext,
51    pub response: Option<Response>,
52    pub error: Error,
53}
54
55type RequestHookFn = Arc<
56    dyn Fn(RequestContext) -> Pin<Box<dyn Future<Output = Result<RequestContext>> + Send>>
57        + Send
58        + Sync,
59>;
60
61type ResponseHookFn = Arc<
62    dyn Fn(ResponseContext) -> Pin<Box<dyn Future<Output = Result<Response>> + Send>> + Send + Sync,
63>;
64
65type SuccessHookFn =
66    Arc<dyn Fn(SuccessContext) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
67
68type ErrorHookFn =
69    Arc<dyn Fn(ErrorContext) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
70
71type RetryHookFn =
72    Arc<dyn Fn(ResponseContext) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
73
74/// Lifecycle hooks for the HTTP client.
75#[derive(Clone, Default)]
76pub struct Hooks {
77    pub(crate) on_request: Vec<RequestHookFn>,
78    pub(crate) on_response: Vec<ResponseHookFn>,
79    pub(crate) on_success: Vec<SuccessHookFn>,
80    pub(crate) on_error: Vec<ErrorHookFn>,
81    pub(crate) on_retry: Vec<RetryHookFn>,
82}
83
84impl Hooks {
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    /// Runs before the transport call. Return `Err(Error::hook("…"))` to cancel the request.
90    pub fn on_request<F, Fut>(mut self, f: F) -> Self
91    where
92        F: Fn(RequestContext) -> Fut + Send + Sync + 'static,
93        Fut: Future<Output = Result<RequestContext>> + Send + 'static,
94    {
95        self.on_request.push(Arc::new(move |ctx| Box::pin(f(ctx))));
96        self
97    }
98
99    /// Runs after the transport returns. Return `Err(Error::hook("…"))` to fail the request.
100    pub fn on_response<F, Fut>(mut self, f: F) -> Self
101    where
102        F: Fn(ResponseContext) -> Fut + Send + Sync + 'static,
103        Fut: Future<Output = Result<Response>> + Send + 'static,
104    {
105        self.on_response.push(Arc::new(move |ctx| Box::pin(f(ctx))));
106        self
107    }
108
109    pub fn on_success<F, Fut>(mut self, f: F) -> Self
110    where
111        F: Fn(SuccessContext) -> Fut + Send + Sync + 'static,
112        Fut: Future<Output = ()> + Send + 'static,
113    {
114        self.on_success.push(Arc::new(move |ctx| Box::pin(f(ctx))));
115        self
116    }
117
118    pub fn on_error<F, Fut>(mut self, f: F) -> Self
119    where
120        F: Fn(ErrorContext) -> Fut + Send + Sync + 'static,
121        Fut: Future<Output = ()> + Send + 'static,
122    {
123        self.on_error.push(Arc::new(move |ctx| Box::pin(f(ctx))));
124        self
125    }
126
127    pub fn on_retry<F, Fut>(mut self, f: F) -> Self
128    where
129        F: Fn(ResponseContext) -> Fut + Send + Sync + 'static,
130        Fut: Future<Output = ()> + Send + 'static,
131    {
132        self.on_retry.push(Arc::new(move |ctx| Box::pin(f(ctx))));
133        self
134    }
135
136    pub(crate) fn merge(mut self, other: Hooks) -> Self {
137        self.on_request.extend(other.on_request);
138        self.on_response.extend(other.on_response);
139        self.on_success.extend(other.on_success);
140        self.on_error.extend(other.on_error);
141        self.on_retry.extend(other.on_retry);
142        self
143    }
144
145    pub(crate) async fn run_on_request(&self, mut ctx: RequestContext) -> Result<RequestContext> {
146        for hook in &self.on_request {
147            ctx = hook(ctx).await?;
148        }
149        Ok(ctx)
150    }
151
152    pub(crate) async fn run_on_response(&self, ctx: ResponseContext) -> Result<Response> {
153        let request = ctx.request;
154        let mut response = ctx.response;
155        for hook in &self.on_response {
156            response = hook(ResponseContext {
157                request: request.clone(),
158                response,
159            })
160            .await?;
161        }
162        Ok(response)
163    }
164
165    pub(crate) async fn run_on_success(&self, ctx: SuccessContext) {
166        for hook in &self.on_success {
167            hook(ctx.clone()).await;
168        }
169    }
170
171    pub(crate) async fn run_on_error(&self, ctx: ErrorContext) {
172        for hook in &self.on_error {
173            hook(ctx.clone()).await;
174        }
175    }
176
177    pub(crate) async fn run_on_retry(&self, ctx: ResponseContext) {
178        for hook in &self.on_retry {
179            hook(ctx.clone()).await;
180        }
181    }
182}