Skip to main content

x402_reqwest/
client.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 http::{Extensions, HeaderMap, StatusCode};
7use reqwest::{Request, Response};
8use reqwest_middleware as rqm;
9use std::sync::Arc;
10use x402_types::proto;
11use x402_types::proto::{OriginalJson, v1, v2};
12use x402_types::scheme::client::{
13    FirstMatch, PaymentCandidate, PaymentSelector, X402Error, X402SchemeClient,
14};
15use x402_types::util::Base64Bytes;
16
17#[cfg(feature = "telemetry")]
18use tracing::{debug, info, instrument, trace};
19
20/// The main x402 client that orchestrates scheme clients and selection.
21///
22/// The [`X402Client`] acts as middleware for reqwest, automatically handling
23/// 402 Payment Required responses by extracting payment requirements, signing
24/// payments, and retrying requests.
25///
26/// ## Creating an X402Client
27///
28/// ```rust
29/// use x402_reqwest::X402Client;
30///
31/// let client = X402Client::new();
32/// ```
33///
34/// ## Registering Scheme Clients
35///
36/// To handle payments on different chains, register scheme clients:
37///
38/// ```rust
39/// use x402_reqwest::X402Client;
40/// use x402_chain_eip155::V1Eip155ExactClient;
41/// use alloy_signer_local::PrivateKeySigner;
42/// use std::sync::Arc;
43///
44/// let private_key_hex = "0x0000000000000000000000000000000000000000000000000000000000000001";
45/// let signer = Arc::new(private_key_hex.parse::<PrivateKeySigner>().unwrap());
46/// let client = X402Client::new()
47///     .register(V1Eip155ExactClient::new(signer));
48/// ```
49///
50/// ## Using with Reqwest
51///
52/// See the [`ReqwestWithPayments`] trait for integrating with reqwest.
53pub struct X402Client<TSelector> {
54    schemes: ClientSchemes,
55    selector: TSelector,
56}
57
58impl X402Client<FirstMatch> {
59    /// Creates a new [`X402Client`] with default settings.
60    ///
61    /// The default client uses [`FirstMatch`] payment selection, which selects
62    /// the first matching payment scheme.
63    pub fn new() -> Self {
64        Self::default()
65    }
66}
67
68impl Default for X402Client<FirstMatch> {
69    fn default() -> Self {
70        Self {
71            schemes: ClientSchemes::default(),
72            selector: FirstMatch,
73        }
74    }
75}
76
77impl<TSelector> X402Client<TSelector> {
78    /// Registers a scheme client for specific chains or networks.
79    ///
80    /// Scheme clients handle the actual payment signing for specific protocols.
81    /// You can register multiple clients for different chains or schemes.
82    ///
83    /// # Arguments
84    ///
85    /// * `scheme` - The scheme client implementation to register
86    ///
87    /// # Returns
88    ///
89    /// A new [`X402Client`] with the additional scheme registered.
90    ///
91    /// # Examples
92    ///
93    /// ```rust
94    /// use x402_reqwest::X402Client;
95    /// use x402_chain_eip155::V1Eip155ExactClient;
96    /// use alloy_signer_local::PrivateKeySigner;
97    /// use std::sync::Arc;
98    ///
99    /// let private_key_hex = "0x0000000000000000000000000000000000000000000000000000000000000001";
100    /// let signer = Arc::new(private_key_hex.parse::<PrivateKeySigner>().unwrap());
101    /// let client = X402Client::new()
102    ///     .register(V1Eip155ExactClient::new(signer));
103    /// ```
104    pub fn register<S>(mut self, scheme: S) -> Self
105    where
106        S: X402SchemeClient + 'static,
107    {
108        self.schemes.push(scheme);
109        self
110    }
111
112    /// Sets a custom payment selector.
113    ///
114    /// By default, [`FirstMatch`] is used which selects the first matching scheme.
115    /// You can implement custom selection logic by providing your own [`PaymentSelector`].
116    ///
117    /// # Examples
118    ///
119    /// ```rust,ignore
120    /// use x402_reqwest::X402Client;
121    /// use x402_types::scheme::client::{FirstMatch, PaymentSelector};
122    ///
123    /// let client = X402Client::new()
124    ///     .with_selector(MyCustomSelector);
125    /// ```
126    pub fn with_selector<P: PaymentSelector + 'static>(self, selector: P) -> X402Client<P> {
127        X402Client {
128            selector,
129            schemes: self.schemes,
130        }
131    }
132}
133
134impl<TSelector> X402Client<TSelector>
135where
136    TSelector: PaymentSelector,
137{
138    /// Creates payment headers from a 402 response.
139    ///
140    /// This method extracts the payment requirements from the response,
141    /// selects the best payment option, signs the payment, and returns
142    /// the appropriate headers to include in the retry request.
143    ///
144    /// # Arguments
145    ///
146    /// * `res` - The 402 Payment Required response
147    ///
148    /// # Returns
149    ///
150    /// A [`HeaderMap`] containing the payment signature header, or an error.
151    ///
152    /// # Errors
153    ///
154    /// Returns [`X402Error::ParseError`] if the response cannot be parsed.
155    /// Returns [`X402Error::NoMatchingPaymentOption`] if no registered scheme
156    /// can handle the payment requirements.
157    #[cfg_attr(
158        feature = "telemetry",
159        instrument(name = "x402.reqwest.make_payment_headers", skip_all, err)
160    )]
161    pub async fn make_payment_headers(&self, res: Response) -> Result<HeaderMap, X402Error> {
162        let payment_required = parse_payment_required(res)
163            .await
164            .ok_or(X402Error::ParseError("Invalid 402 response".to_string()))?;
165        let candidates = self.schemes.candidates(&payment_required);
166
167        // Select the best candidate
168        let selected = self
169            .selector
170            .select(&candidates)
171            .ok_or(X402Error::NoMatchingPaymentOption)?;
172
173        #[cfg(feature = "telemetry")]
174        debug!(
175            scheme = %selected.scheme,
176            chain_id = %selected.chain_id,
177            "Selected payment scheme"
178        );
179
180        let signed_payload = selected.sign().await?;
181        let header_name = match &payment_required {
182            proto::PaymentRequired::V1(_) => "X-Payment",
183            proto::PaymentRequired::V2(_) => "Payment-Signature",
184        };
185        let headers = {
186            let mut headers = HeaderMap::new();
187            headers.insert(header_name, signed_payload.parse().unwrap());
188            headers
189        };
190
191        Ok(headers)
192    }
193}
194
195/// Internal collection of registered scheme clients.
196#[derive(Default)]
197pub struct ClientSchemes(Vec<Arc<dyn X402SchemeClient>>);
198
199impl ClientSchemes {
200    /// Adds a scheme client to the collection.
201    pub fn push<T: X402SchemeClient + 'static>(&mut self, client: T) {
202        self.0.push(Arc::new(client));
203    }
204
205    /// Finds all payment candidates that can handle the given payment requirements.
206    pub fn candidates(&self, payment_required: &proto::PaymentRequired) -> Vec<PaymentCandidate> {
207        let mut candidates = vec![];
208        for client in self.0.iter() {
209            let accepted = client.accept(payment_required);
210            candidates.extend(accepted);
211        }
212        candidates
213    }
214}
215
216/// Runs the next middleware or HTTP client with optional telemetry instrumentation.
217#[cfg_attr(
218    feature = "telemetry",
219    instrument(name = "x402.reqwest.next", skip_all)
220)]
221async fn run_next(
222    next: rqm::Next<'_>,
223    req: Request,
224    extensions: &mut Extensions,
225) -> rqm::Result<Response> {
226    next.run(req, extensions).await
227}
228
229#[async_trait::async_trait]
230impl<TSelector> rqm::Middleware for X402Client<TSelector>
231where
232    TSelector: PaymentSelector + Send + Sync + 'static,
233{
234    /// Handles a request, automatically handling 402 responses.
235    ///
236    /// When a 402 response is received, this middleware:
237    /// 1. Extracts payment requirements from the response
238    /// 2. Signs a payment using registered scheme clients
239    /// 3. Retries the request with the payment header
240    #[cfg_attr(
241        feature = "telemetry",
242        instrument(name = "x402.reqwest.handle", skip_all, err)
243    )]
244    async fn handle(
245        &self,
246        req: Request,
247        extensions: &mut Extensions,
248        next: rqm::Next<'_>,
249    ) -> rqm::Result<Response> {
250        let retry_req = req.try_clone();
251        let res = run_next(next.clone(), req, extensions).await?;
252
253        if res.status() != StatusCode::PAYMENT_REQUIRED {
254            #[cfg(feature = "telemetry")]
255            trace!(status = ?res.status(), "No payment required, returning response");
256            return Ok(res);
257        }
258
259        #[cfg(feature = "telemetry")]
260        info!(url = ?res.url(), "Received 402 Payment Required, processing payment");
261
262        let headers = self
263            .make_payment_headers(res)
264            .await
265            .map_err(|e| rqm::Error::Middleware(e.into()))?;
266
267        // Retry with payment
268        let mut retry = retry_req.ok_or(rqm::Error::Middleware(
269            X402Error::RequestNotCloneable.into(),
270        ))?;
271        retry.headers_mut().extend(headers);
272
273        #[cfg(feature = "telemetry")]
274        trace!(url = ?retry.url(), "Retrying request with payment headers");
275
276        run_next(next, retry, extensions).await
277    }
278}
279
280/// Parses a 402 Payment Required response into a [`proto::PaymentRequired`].
281///
282/// Supports both V1 (JSON body) and V2 (base64-encoded header) formats.
283#[cfg_attr(
284    feature = "telemetry",
285    instrument(name = "x402.reqwest.parse_payment_required", skip(response))
286)]
287pub async fn parse_payment_required(response: Response) -> Option<proto::PaymentRequired> {
288    // Try V2 format first (header-based)
289    let headers = response.headers();
290    let v2_payment_required = headers
291        .get("Payment-Required")
292        .and_then(|h| Base64Bytes::from(h.as_bytes()).decode().ok())
293        .and_then(|b| serde_json::from_slice::<v2::PaymentRequired<OriginalJson>>(&b).ok());
294    if let Some(v2_payment_required) = v2_payment_required {
295        #[cfg(feature = "telemetry")]
296        debug!("Parsed V2 payment required from header");
297        return Some(proto::PaymentRequired::V2(v2_payment_required));
298    }
299
300    // Fall back to V1 format (body-based)
301    let v1_payment_required = response
302        .bytes()
303        .await
304        .ok()
305        .and_then(|b| serde_json::from_slice::<v1::PaymentRequired<OriginalJson>>(&b).ok());
306    if let Some(v1_payment_required) = v1_payment_required {
307        #[cfg(feature = "telemetry")]
308        debug!("Parsed V1 payment required from body");
309        return Some(proto::PaymentRequired::V1(v1_payment_required));
310    }
311
312    #[cfg(feature = "telemetry")]
313    debug!("Could not parse payment required from response");
314
315    None
316}