Skip to main content

r402_http/client/
middleware.rs

1//! Client-side x402 payment handling for reqwest.
2//!
3//! This module provides the [`X402Client`] which orchestrates scheme clients
4//! and payment selection for automatic payment handling.
5
6use std::sync::Arc;
7
8use http::{Extensions, HeaderMap, StatusCode};
9use r402::hooks::{FailureRecovery, HookDecision};
10use r402::proto;
11use r402::proto::Base64Bytes;
12use r402::proto::v2;
13use r402::scheme::{
14    ClientError, FirstMatch, PaymentCandidate, PaymentPolicy, PaymentSelector, SchemeClient,
15};
16use reqwest::{Request, Response};
17use reqwest_middleware as rqm;
18#[cfg(feature = "telemetry")]
19use tracing::{debug, info, instrument, trace};
20
21use super::hooks::{ClientHooks, PaymentCreationContext};
22
23/// The main x402 client that orchestrates scheme clients and selection.
24///
25/// The [`X402Client`] acts as middleware for reqwest, automatically handling
26/// 402 Payment Required responses by extracting payment requirements, signing
27/// payments, and retrying requests.
28#[allow(
29    missing_debug_implementations,
30    reason = "ClientSchemes contains dyn trait objects"
31)]
32pub struct X402Client<TSelector> {
33    schemes: ClientSchemes,
34    selector: TSelector,
35    policies: Vec<Arc<dyn PaymentPolicy>>,
36    hooks: Arc<[Arc<dyn ClientHooks>]>,
37}
38
39impl X402Client<FirstMatch> {
40    /// Creates a new [`X402Client`] with default settings.
41    ///
42    /// The default client uses [`FirstMatch`] payment selection, which selects
43    /// the first matching payment scheme.
44    #[must_use]
45    pub fn new() -> Self {
46        Self::default()
47    }
48}
49
50impl Default for X402Client<FirstMatch> {
51    fn default() -> Self {
52        Self {
53            schemes: ClientSchemes::default(),
54            selector: FirstMatch,
55            policies: Vec::new(),
56            hooks: Arc::from([]),
57        }
58    }
59}
60
61impl<TSelector> X402Client<TSelector> {
62    /// Registers a scheme client for specific chains or networks.
63    ///
64    /// Scheme clients handle the actual payment signing for specific protocols.
65    /// You can register multiple clients for different chains or schemes.
66    ///
67    /// # Arguments
68    ///
69    /// * `scheme` - The scheme client implementation to register
70    ///
71    /// # Returns
72    ///
73    /// A new [`X402Client`] with the additional scheme registered.
74    #[must_use]
75    pub fn register<S>(mut self, scheme: S) -> Self
76    where
77        S: SchemeClient + 'static,
78    {
79        self.schemes.push(scheme);
80        self
81    }
82
83    /// Sets a custom payment selector.
84    ///
85    /// By default, [`FirstMatch`] is used which selects the first matching scheme.
86    /// You can implement custom selection logic by providing your own [`PaymentSelector`].
87    pub fn with_selector<P: PaymentSelector + 'static>(self, selector: P) -> X402Client<P> {
88        X402Client {
89            selector,
90            schemes: self.schemes,
91            policies: self.policies,
92            hooks: self.hooks,
93        }
94    }
95
96    /// Adds a payment policy to the filtering pipeline.
97    ///
98    /// Policies are applied in registration order before the selector picks
99    /// the final candidate. Use policies to restrict which networks, schemes,
100    /// or amounts are acceptable.
101    #[must_use]
102    pub fn with_policy<P: PaymentPolicy + 'static>(mut self, policy: P) -> Self {
103        self.policies.push(Arc::new(policy));
104        self
105    }
106
107    /// Adds a lifecycle hook for payment creation.
108    ///
109    /// Hooks allow intercepting the payment creation pipeline for logging,
110    /// custom validation, or error recovery. Multiple hooks are executed
111    /// in registration order.
112    #[must_use]
113    pub fn with_hook(mut self, hook: impl ClientHooks + 'static) -> Self {
114        let mut hooks = (*self.hooks).to_vec();
115        hooks.push(Arc::new(hook));
116        self.hooks = Arc::from(hooks);
117        self
118    }
119}
120
121impl<TSelector> X402Client<TSelector>
122where
123    TSelector: PaymentSelector,
124{
125    /// Creates payment headers from a 402 response.
126    ///
127    /// This method extracts the payment requirements from the response,
128    /// selects the best payment option, signs the payment, and returns
129    /// the appropriate headers to include in the retry request.
130    ///
131    /// # Arguments
132    ///
133    /// * `res` - The 402 Payment Required response
134    ///
135    /// # Returns
136    ///
137    /// A [`HeaderMap`] containing the payment signature header, or an error.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`ClientError::ParseError`] if the response cannot be parsed.
142    /// Returns [`ClientError::NoMatchingPaymentOption`] if no registered scheme
143    /// can handle the payment requirements.
144    ///
145    /// # Panics
146    ///
147    /// Panics if the signed payload is not a valid HTTP header value.
148    #[cfg_attr(
149        feature = "telemetry",
150        instrument(name = "x402.reqwest.make_payment_headers", skip_all, err)
151    )]
152    pub async fn make_payment_headers(&self, res: Response) -> Result<HeaderMap, ClientError> {
153        let payment_required = parse_payment_required(res)
154            .await
155            .ok_or_else(|| ClientError::ParseError("Invalid 402 response".to_owned()))?;
156
157        let hook_ctx = PaymentCreationContext {
158            payment_required: payment_required.clone(),
159        };
160
161        // Phase 1: Before hooks — first abort wins
162        for hook in self.hooks.iter() {
163            if let HookDecision::Abort { reason, .. } =
164                hook.before_payment_creation(&hook_ctx).await
165            {
166                return Err(ClientError::ParseError(reason));
167            }
168        }
169
170        let creation_result = self.create_payment_headers_inner(&payment_required).await;
171
172        match creation_result {
173            Ok(headers) => {
174                // Phase 3a: After hooks (fire-and-forget)
175                for hook in self.hooks.iter() {
176                    hook.after_payment_creation(&hook_ctx, &headers).await;
177                }
178                Ok(headers)
179            }
180            Err(err) => {
181                // Phase 3b: Failure hooks — first recovery wins
182                let err_msg = err.to_string();
183                for hook in self.hooks.iter() {
184                    if let FailureRecovery::Recovered(headers) =
185                        hook.on_payment_creation_failure(&hook_ctx, &err_msg).await
186                    {
187                        return Ok(headers);
188                    }
189                }
190                Err(err)
191            }
192        }
193    }
194
195    /// Internal helper that performs the actual payment header creation.
196    async fn create_payment_headers_inner(
197        &self,
198        payment_required: &proto::PaymentRequired,
199    ) -> Result<HeaderMap, ClientError> {
200        let candidates = self.schemes.candidates(payment_required);
201
202        // Apply policies to filter candidates
203        let mut filtered: Vec<&PaymentCandidate> = candidates.iter().collect();
204        for policy in &self.policies {
205            filtered = policy.apply(filtered);
206            if filtered.is_empty() {
207                return Err(ClientError::NoMatchingPaymentOption);
208            }
209        }
210
211        // Select the best candidate from filtered list
212        let selected = self
213            .selector
214            .select(&filtered)
215            .ok_or(ClientError::NoMatchingPaymentOption)?;
216
217        #[cfg(feature = "telemetry")]
218        debug!(
219            scheme = %selected.scheme,
220            chain_id = %selected.chain_id,
221            "Selected payment scheme"
222        );
223
224        let signed_payload = selected.sign().await?;
225        let headers = {
226            let mut headers = HeaderMap::new();
227            #[allow(
228                clippy::expect_used,
229                reason = "base64-encoded payload is always valid ASCII header"
230            )]
231            headers.insert(
232                "Payment-Signature",
233                signed_payload
234                    .parse()
235                    .expect("signed payload is valid header value"),
236            );
237            headers
238        };
239
240        Ok(headers)
241    }
242}
243
244/// Internal collection of registered scheme clients.
245#[derive(Default)]
246#[allow(
247    missing_debug_implementations,
248    reason = "dyn trait objects do not impl Debug"
249)]
250pub(super) struct ClientSchemes(Vec<Arc<dyn SchemeClient>>);
251
252impl ClientSchemes {
253    /// Adds a scheme client to the collection.
254    pub(super) fn push<T: SchemeClient + 'static>(&mut self, client: T) {
255        self.0.push(Arc::new(client));
256    }
257
258    /// Finds all payment candidates that can handle the given payment requirements.
259    #[must_use]
260    pub(super) fn candidates(
261        &self,
262        payment_required: &proto::PaymentRequired,
263    ) -> Vec<PaymentCandidate> {
264        let mut candidates = vec![];
265        for client in &self.0 {
266            let accepted = client.accept(payment_required);
267            candidates.extend(accepted);
268        }
269        candidates
270    }
271}
272
273/// Runs the next middleware or HTTP client with optional telemetry instrumentation.
274#[cfg_attr(
275    feature = "telemetry",
276    instrument(name = "x402.reqwest.next", skip_all)
277)]
278async fn run_next(
279    next: rqm::Next<'_>,
280    req: Request,
281    extensions: &mut Extensions,
282) -> rqm::Result<Response> {
283    next.run(req, extensions).await
284}
285
286#[async_trait::async_trait]
287impl<TSelector> rqm::Middleware for X402Client<TSelector>
288where
289    TSelector: PaymentSelector + Send + Sync + 'static,
290{
291    /// Handles a request, automatically handling 402 responses.
292    ///
293    /// When a 402 response is received, this middleware:
294    /// 1. Extracts payment requirements from the response
295    /// 2. Signs a payment using registered scheme clients
296    /// 3. Retries the request with the payment header
297    ///
298    /// If the request body is not cloneable (e.g. streaming), the middleware
299    /// cannot auto-retry after a 402. In that case the original 402 response
300    /// is returned as-is so the caller can handle it manually.
301    #[cfg_attr(
302        feature = "telemetry",
303        instrument(name = "x402.reqwest.handle", skip_all, err)
304    )]
305    async fn handle(
306        &self,
307        req: Request,
308        extensions: &mut Extensions,
309        next: rqm::Next<'_>,
310    ) -> rqm::Result<Response> {
311        let retry_req = req.try_clone();
312        let res = run_next(next.clone(), req, extensions).await?;
313
314        if res.status() != StatusCode::PAYMENT_REQUIRED {
315            #[cfg(feature = "telemetry")]
316            trace!(status = ?res.status(), "No payment required, returning response");
317            return Ok(res);
318        }
319
320        #[cfg(feature = "telemetry")]
321        info!(url = ?res.url(), "Received 402 Payment Required, processing payment");
322
323        // If the original request is not cloneable (streaming body), we cannot
324        // auto-retry. Return the 402 response for manual handling by the caller.
325        let Some(mut retry) = retry_req else {
326            #[cfg(feature = "telemetry")]
327            tracing::warn!("Cannot auto-retry 402: request body not cloneable, returning raw 402");
328            return Ok(res);
329        };
330
331        let headers = self
332            .make_payment_headers(res)
333            .await
334            .map_err(|e| rqm::Error::Middleware(e.into()))?;
335
336        retry.headers_mut().extend(headers);
337
338        #[cfg(feature = "telemetry")]
339        trace!(url = ?retry.url(), "Retrying request with payment headers");
340
341        run_next(next, retry, extensions).await
342    }
343}
344
345/// Parses a 402 Payment Required response into a [`proto::PaymentRequired`].
346///
347/// Tries to extract V2 payment requirements from the `Payment-Required` header
348/// (base64-encoded JSON) first, then falls back to parsing the response body as
349/// plain JSON. This matches the Go SDK's `handleV2Payment` which also tries
350/// header first then body.
351#[cfg_attr(
352    feature = "telemetry",
353    instrument(name = "x402.reqwest.parse_payment_required", skip(response))
354)]
355pub async fn parse_payment_required(response: Response) -> Option<proto::PaymentRequired> {
356    let v2_from_header = response
357        .headers()
358        .get("Payment-Required")
359        .and_then(|h| Base64Bytes::from(h.as_bytes()).decode().ok())
360        .and_then(|b| serde_json::from_slice::<v2::PaymentRequired>(&b).ok());
361
362    if let Some(v2_payment_required) = v2_from_header {
363        #[cfg(feature = "telemetry")]
364        debug!("Parsed V2 payment required from header");
365        return Some(v2_payment_required);
366    }
367
368    // Fall back to body (some servers send PaymentRequired as JSON body)
369    if let Ok(body_bytes) = response.bytes().await
370        && let Ok(v2_from_body) = serde_json::from_slice::<v2::PaymentRequired>(&body_bytes)
371    {
372        #[cfg(feature = "telemetry")]
373        debug!("Parsed V2 payment required from response body");
374        return Some(v2_from_body);
375    }
376
377    #[cfg(feature = "telemetry")]
378    debug!("Could not parse payment required from response");
379
380    None
381}