1use http::{Extensions, HeaderMap, StatusCode};
9use r402_core::ClientHooks;
10use r402_core::client::{CreatedPayment, PaymentClient, PaymentResponseContext};
11use r402_core::error::ClientError;
12use r402_core::scheme::{FirstMatch, PaymentPolicy, PaymentSelector, SchemeClient};
13use r402_core::wire;
14use r402_core::wire::Base64Bytes;
15use reqwest::{Client, Request, Response};
16use reqwest_middleware as rqm;
17#[cfg(feature = "telemetry")]
18use tracing::{debug, info, instrument, trace};
19
20#[allow(
25 missing_debug_implementations,
26 reason = "PaymentClient contains dyn trait objects"
27)]
28pub struct X402Client<TSelector = FirstMatch> {
29 inner: PaymentClient<TSelector>,
30}
31
32impl X402Client<FirstMatch> {
33 #[must_use]
35 pub fn new() -> Self {
36 Self::default()
37 }
38}
39
40impl Default for X402Client<FirstMatch> {
41 fn default() -> Self {
42 Self {
43 inner: PaymentClient::new(),
44 }
45 }
46}
47
48impl<TSelector> X402Client<TSelector> {
49 #[must_use]
51 pub const fn from_payment_client(inner: PaymentClient<TSelector>) -> Self {
52 Self { inner }
53 }
54
55 #[must_use]
57 pub const fn payment_client(&self) -> &PaymentClient<TSelector> {
58 &self.inner
59 }
60
61 #[must_use]
63 pub fn register<S>(mut self, scheme: S) -> Self
64 where
65 S: SchemeClient + 'static,
66 {
67 self.inner = self.inner.register(scheme);
68 self
69 }
70
71 #[must_use]
73 pub fn with_selector<P: PaymentSelector>(self, selector: P) -> X402Client<P> {
74 X402Client {
75 inner: self.inner.with_selector(selector),
76 }
77 }
78
79 #[must_use]
81 pub fn with_policy<P: PaymentPolicy + 'static>(mut self, policy: P) -> Self {
82 self.inner = self.inner.with_policy(policy);
83 self
84 }
85
86 #[must_use]
88 pub fn with_hook(mut self, hook: impl ClientHooks + 'static) -> Self {
89 self.inner = self.inner.with_hook(hook);
90 self
91 }
92}
93
94impl<TSelector> X402Client<TSelector>
95where
96 TSelector: PaymentSelector,
97{
98 #[cfg_attr(
104 feature = "telemetry",
105 instrument(name = "x402.reqwest.make_payment_headers", skip_all, err)
106 )]
107 pub async fn make_payment_headers(&self, res: Response) -> Result<HeaderMap, ClientError> {
108 let payment_required = parse_payment_required(res)
109 .await
110 .ok_or_else(|| ClientError::Parse("Invalid 402 response".to_owned()))?;
111 let created = self.inner.create_payment(&payment_required).await?;
112 Ok(payment_signature_headers(&created))
113 }
114
115 pub async fn create_payment(
121 &self,
122 payment_required: &wire::PaymentRequired,
123 ) -> Result<CreatedPayment, ClientError> {
124 self.inner.create_payment(payment_required).await
125 }
126}
127
128#[must_use]
135pub fn payment_signature_headers(created: &CreatedPayment) -> HeaderMap {
136 let mut headers = HeaderMap::new();
137 #[allow(
138 clippy::expect_used,
139 reason = "base64-encoded payload is always valid ASCII header"
140 )]
141 headers.insert(
142 "Payment-Signature",
143 created
144 .signed_payload
145 .parse()
146 .expect("signed payload is valid header value"),
147 );
148 headers
149}
150
151#[cfg_attr(
153 feature = "telemetry",
154 instrument(name = "x402.reqwest.next", skip_all)
155)]
156async fn run_next(
157 next: rqm::Next<'_>,
158 req: Request,
159 extensions: &mut Extensions,
160) -> rqm::Result<Response> {
161 next.run(req, extensions).await
162}
163
164#[async_trait::async_trait]
165impl<TSelector> rqm::Middleware for X402Client<TSelector>
166where
167 TSelector: PaymentSelector + Send + Sync + 'static,
168{
169 #[cfg_attr(
177 feature = "telemetry",
178 instrument(name = "x402.reqwest.handle", skip_all, err)
179 )]
180 async fn handle(
181 &self,
182 req: Request,
183 extensions: &mut Extensions,
184 next: rqm::Next<'_>,
185 ) -> rqm::Result<Response> {
186 let retry_template = req.try_clone();
187 let res = run_next(next.clone(), req, extensions).await?;
188
189 if res.status() != StatusCode::PAYMENT_REQUIRED {
190 #[cfg(feature = "telemetry")]
191 trace!(status = ?res.status(), "No payment required, returning response");
192 return Ok(res);
193 }
194
195 #[cfg(feature = "telemetry")]
196 info!(url = ?res.url(), "Received 402 Payment Required, processing payment");
197
198 let Some(template) = retry_template else {
199 #[cfg(feature = "telemetry")]
200 tracing::warn!("Cannot auto-retry 402: request body not cloneable, returning raw 402");
201 return Ok(res);
202 };
203
204 let payment_required = parse_payment_required(res).await.ok_or_else(|| {
205 rqm::Error::Middleware(ClientError::Parse("Invalid 402 response".into()).into())
206 })?;
207
208 let created = self
209 .inner
210 .create_payment(&payment_required)
211 .await
212 .map_err(|e| rqm::Error::Middleware(e.into()))?;
213
214 let paid = clone_with_payment(&template, &created)
215 .map_err(|e| rqm::Error::Middleware(e.into()))?;
216
217 #[cfg(feature = "telemetry")]
218 trace!(url = ?paid.url(), "Retrying request with payment headers");
219
220 let response = run_next(next.clone(), paid, extensions).await?;
221
222 let Some(recovered) = self
223 .dispatch_and_maybe_recover(&payment_required, &created, &response)
224 .await
225 .map_err(|e| rqm::Error::Middleware(e.into()))?
226 else {
227 return Ok(response);
228 };
229
230 let second = clone_with_payment(&template, &recovered)
231 .map_err(|e| rqm::Error::Middleware(e.into()))?;
232 let second_response = run_next(next, second, extensions).await?;
233 let ctx = build_response_context(&payment_required, &recovered, &second_response);
235 if ctx.settle_response.is_some() || ctx.corrective_payment_required.is_some() {
236 let _ = self.inner.handle_payment_response(&ctx).await;
237 }
238 Ok(second_response)
239 }
240}
241
242impl<TSelector: PaymentSelector> X402Client<TSelector> {
243 async fn dispatch_and_maybe_recover(
246 &self,
247 original: &wire::PaymentRequired,
248 created: &CreatedPayment,
249 response: &Response,
250 ) -> Result<Option<CreatedPayment>, ClientError> {
251 let ctx = build_response_context(original, created, response);
252 if ctx.settle_response.is_none() && ctx.corrective_payment_required.is_none() {
253 return Ok(None);
254 }
255
256 let result = self.inner.handle_payment_response(&ctx).await;
257 if !result.recovered {
258 return Ok(None);
259 }
260
261 let challenge = ctx.corrective_payment_required.as_ref().unwrap_or(original);
262 let fresh = self.inner.create_payment(challenge).await?;
263 Ok(Some(fresh))
264 }
265}
266
267fn build_response_context(
268 original: &wire::PaymentRequired,
269 created: &CreatedPayment,
270 response: &Response,
271) -> PaymentResponseContext {
272 let settle_response = response
273 .headers()
274 .get("Payment-Response")
275 .and_then(|h| Base64Bytes::from(h.as_bytes()).decode().ok())
276 .and_then(|b| serde_json::from_slice(&b).ok());
277
278 let corrective_payment_required =
279 if settle_response.is_none() && response.status() == StatusCode::PAYMENT_REQUIRED {
280 response
281 .headers()
282 .get("Payment-Required")
283 .and_then(|h| Base64Bytes::from(h.as_bytes()).decode().ok())
284 .and_then(|b| serde_json::from_slice(&b).ok())
285 } else {
286 None
287 };
288
289 let mut ctx = PaymentResponseContext::new(original.clone(), created.signed_payload.clone());
290 if let Some(settle) = settle_response {
291 ctx = ctx.with_settle_response(settle);
292 }
293 if let Some(required) = corrective_payment_required {
294 ctx = ctx.with_corrective_payment_required(required);
295 }
296 ctx
297}
298
299fn clone_with_payment(
300 template: &Request,
301 created: &CreatedPayment,
302) -> Result<Request, ClientError> {
303 let mut req = template
304 .try_clone()
305 .ok_or(ClientError::RequestNotCloneable)?;
306 req.headers_mut().extend(payment_signature_headers(created));
307 Ok(req)
308}
309
310#[cfg_attr(
314 feature = "telemetry",
315 instrument(name = "x402.reqwest.parse_payment_required", skip(response))
316)]
317pub async fn parse_payment_required(response: Response) -> Option<wire::PaymentRequired> {
318 let v2_from_header = response
319 .headers()
320 .get("Payment-Required")
321 .and_then(|h| Base64Bytes::from(h.as_bytes()).decode().ok())
322 .and_then(|b| serde_json::from_slice::<wire::PaymentRequired>(&b).ok());
323
324 if let Some(v2_payment_required) = v2_from_header {
325 #[cfg(feature = "telemetry")]
326 debug!("Parsed V2 payment required from header");
327 return Some(v2_payment_required);
328 }
329
330 if let Ok(body_bytes) = response.bytes().await
331 && let Ok(v2_from_body) = serde_json::from_slice::<wire::PaymentRequired>(&body_bytes)
332 {
333 #[cfg(feature = "telemetry")]
334 debug!("Parsed V2 payment required from response body");
335 return Some(v2_from_body);
336 }
337
338 #[cfg(feature = "telemetry")]
339 debug!("Could not parse payment required from response");
340
341 None
342}
343
344impl<S> X402Client<S>
345where
346 Self: rqm::Middleware,
347{
348 #[must_use]
353 pub fn wrap(self, client: Client) -> rqm::ClientWithMiddleware {
354 rqm::ClientBuilder::new(client).with(self).build()
355 }
356
357 #[must_use]
360 pub fn wrap_builder(self, client: Client) -> rqm::ClientBuilder {
361 rqm::ClientBuilder::new(client).with(self)
362 }
363}
364
365pub trait WithPayments {
373 fn with_payments<S>(self, x402: X402Client<S>) -> rqm::ClientWithMiddleware
375 where
376 X402Client<S>: rqm::Middleware;
377}
378
379impl WithPayments for Client {
380 fn with_payments<S>(self, x402: X402Client<S>) -> rqm::ClientWithMiddleware
381 where
382 X402Client<S>: rqm::Middleware,
383 {
384 x402.wrap(self)
385 }
386}