Skip to main content

better_fetch/
hooks.rs

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