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}