1use std::fmt::{self, Debug, Formatter};
4
5use http::{Extensions, HeaderMap, StatusCode};
6use r402_client::{
7 ClientExtension, ClientHooks, CreatedPayment, FirstMatch, PaymentClient, PaymentPolicy,
8 PaymentResponseContext, PaymentSelector, SchemeClient, SpendControls,
9};
10use r402_protocol::ClientError;
11use r402_protocol::payment::PaymentRequired;
12use reqwest::{Client, Request, Response};
13use reqwest_middleware as rqm;
14
15use super::signature::{
16 clone_with_payment, parse_payment_required, payment_required_from_headers,
17 payment_signature_headers, settle_response_from_headers,
18};
19
20pub struct X402Client<TSelector = FirstMatch> {
26 inner: PaymentClient<TSelector>,
27}
28
29impl X402Client<FirstMatch> {
30 #[must_use]
32 pub fn new() -> Self {
33 Self::default()
34 }
35}
36
37impl Default for X402Client<FirstMatch> {
38 fn default() -> Self {
39 Self {
40 inner: PaymentClient::new(),
41 }
42 }
43}
44
45impl<TSelector> Debug for X402Client<TSelector> {
46 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
47 f.debug_struct("X402Client")
48 .field("inner", &self.inner)
49 .finish()
50 }
51}
52
53impl<TSelector> X402Client<TSelector> {
54 #[must_use]
56 pub const fn from_payment_client(inner: PaymentClient<TSelector>) -> Self {
57 Self { inner }
58 }
59
60 #[must_use]
62 pub const fn payment_client(&self) -> &PaymentClient<TSelector> {
63 &self.inner
64 }
65
66 #[must_use]
68 pub fn register(mut self, scheme: impl SchemeClient + 'static) -> Self {
69 self.inner = self.inner.register(scheme);
70 self
71 }
72
73 #[must_use]
75 pub fn with_selector<P: PaymentSelector>(self, selector: P) -> X402Client<P> {
76 X402Client {
77 inner: self.inner.with_selector(selector),
78 }
79 }
80
81 #[must_use]
83 pub fn with_policy(mut self, policy: impl PaymentPolicy + 'static) -> Self {
84 self.inner = self.inner.with_policy(policy);
85 self
86 }
87
88 #[must_use]
90 pub fn with_hook(mut self, hook: impl ClientHooks + 'static) -> Self {
91 self.inner = self.inner.with_hook(hook);
92 self
93 }
94
95 #[must_use]
97 pub fn with_extension(mut self, extension: impl ClientExtension + 'static) -> Self {
98 self.inner = self.inner.with_extension(extension);
99 self
100 }
101
102 #[must_use]
104 pub fn with_spend_controls(mut self, controls: SpendControls) -> Self {
105 self.inner = self.inner.with_spend_controls(controls);
106 self
107 }
108
109 #[must_use]
111 pub fn disable_spend_controls(mut self) -> Self {
112 self.inner = self.inner.disable_spend_controls();
113 self
114 }
115}
116
117impl<TSelector> X402Client<TSelector>
118where
119 TSelector: PaymentSelector,
120{
121 pub async fn make_payment_headers(&self, res: Response) -> Result<HeaderMap, ClientError> {
130 let request_url = res.url().clone();
131 let payment_required = parse_payment_required(res)
132 .await
133 .ok_or_else(|| ClientError::Parse("Invalid 402 response".to_owned()))?;
134 let created = self.inner.create_payment(&payment_required).await?;
135 let mut headers = payment_signature_headers(&created)?;
136 headers.extend(
137 self.inner
138 .extension_headers(&payment_required, request_url.as_str())
139 .await,
140 );
141 Ok(headers)
142 }
143
144 pub async fn create_payment(
150 &self,
151 payment_required: &PaymentRequired,
152 ) -> Result<CreatedPayment, ClientError> {
153 self.inner.create_payment(payment_required).await
154 }
155}
156
157impl<S> X402Client<S>
158where
159 Self: rqm::Middleware,
160{
161 #[must_use]
163 pub fn wrap(self, client: Client) -> rqm::ClientWithMiddleware {
164 rqm::ClientBuilder::new(client).with(self).build()
165 }
166
167 #[must_use]
169 pub fn wrap_builder(self, client: Client) -> rqm::ClientBuilder {
170 rqm::ClientBuilder::new(client).with(self)
171 }
172}
173
174pub trait WithPayments {
176 #[must_use]
178 fn with_payments<S>(self, x402: X402Client<S>) -> rqm::ClientWithMiddleware
179 where
180 X402Client<S>: rqm::Middleware;
181}
182
183impl WithPayments for Client {
184 fn with_payments<S>(self, x402: X402Client<S>) -> rqm::ClientWithMiddleware
185 where
186 X402Client<S>: rqm::Middleware,
187 {
188 x402.wrap(self)
189 }
190}
191
192fn should_retry(status: StatusCode) -> bool {
195 status == StatusCode::PAYMENT_REQUIRED
196}
197
198#[async_trait::async_trait]
199impl<TSelector> rqm::Middleware for X402Client<TSelector>
200where
201 TSelector: PaymentSelector + Send + Sync + 'static,
202{
203 async fn handle(
204 &self,
205 req: Request,
206 extensions: &mut Extensions,
207 next: rqm::Next<'_>,
208 ) -> rqm::Result<Response> {
209 let retry_template = req.try_clone();
210 let res = next.clone().run(req, extensions).await?;
211 if !should_retry(res.status()) {
212 return Ok(res);
213 }
214
215 let Some(template) = retry_template else {
216 return Ok(res);
217 };
218
219 let request_url = res.url().clone();
220 let payment_required = parse_payment_required(res)
221 .await
222 .ok_or_else(|| middleware_err(ClientError::Parse("Invalid 402 response".into())))?;
223 let created = self
224 .inner
225 .create_payment(&payment_required)
226 .await
227 .map_err(middleware_err)?;
228 let paid = clone_with_payment_and_extensions(
229 &template,
230 &created,
231 &payment_required,
232 request_url.as_str(),
233 &self.inner,
234 )
235 .await
236 .map_err(middleware_err)?;
237 let response = next.clone().run(paid, extensions).await?;
238
239 let Some(recovered) = self
240 .dispatch_and_maybe_recover(&payment_required, &created, &response)
241 .await
242 .map_err(middleware_err)?
243 else {
244 return Ok(response);
245 };
246
247 let second = clone_with_payment_and_extensions(
248 &template,
249 &recovered,
250 &recovered.payment_required,
251 response.url().as_str(),
252 &self.inner,
253 )
254 .await
255 .map_err(middleware_err)?;
256 let second_response = next.run(second, extensions).await?;
257 let ctx = build_response_context(&payment_required, &recovered, &second_response);
258 if ctx.settle_response.is_some() || ctx.corrective_payment_required.is_some() {
259 self.inner.handle_payment_response(&ctx).await;
260 }
261 Ok(second_response)
262 }
263}
264
265impl<TSelector: PaymentSelector> X402Client<TSelector> {
266 async fn dispatch_and_maybe_recover(
269 &self,
270 original: &PaymentRequired,
271 created: &CreatedPayment,
272 response: &Response,
273 ) -> Result<Option<CreatedPayment>, ClientError> {
274 let ctx = build_response_context(original, created, response);
275 if ctx.settle_response.is_none() && ctx.corrective_payment_required.is_none() {
276 return Ok(None);
277 }
278
279 let result = self.inner.handle_payment_response(&ctx).await;
280 if !result.recovered {
281 return Ok(None);
282 }
283
284 let challenge = ctx.corrective_payment_required.as_ref().unwrap_or(original);
285 let fresh = self.inner.create_payment(challenge).await?;
286 Ok(Some(fresh))
287 }
288}
289
290async fn clone_with_payment_and_extensions<TSelector: PaymentSelector>(
291 template: &Request,
292 created: &CreatedPayment,
293 payment_required: &PaymentRequired,
294 request_url: &str,
295 client: &PaymentClient<TSelector>,
296) -> Result<Request, ClientError> {
297 let mut paid = clone_with_payment(template, created)?;
298 paid.headers_mut().extend(
299 client
300 .extension_headers(payment_required, request_url)
301 .await,
302 );
303 Ok(paid)
304}
305
306fn build_response_context(
307 original: &PaymentRequired,
308 created: &CreatedPayment,
309 response: &Response,
310) -> PaymentResponseContext {
311 let settle_response = settle_response_from_headers(response.headers());
312 let corrective_payment_required =
313 if settle_response.is_none() && should_retry(response.status()) {
314 payment_required_from_headers(response.headers())
315 } else {
316 None
317 };
318
319 let mut ctx = PaymentResponseContext::new(original.clone(), created.signed_payload.clone());
320 if let Some(settle) = settle_response {
321 ctx = ctx.with_settle_response(settle);
322 }
323 if let Some(required) = corrective_payment_required {
324 ctx = ctx.with_corrective_payment_required(required);
325 }
326 ctx
327}
328
329fn middleware_err(err: ClientError) -> rqm::Error {
330 rqm::Error::middleware(err)
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn retries_only_status_402() {
339 assert!(
340 should_retry(StatusCode::PAYMENT_REQUIRED),
341 "402 is the only auto-retry status"
342 );
343 assert!(
344 !should_retry(StatusCode::PRECONDITION_FAILED),
345 "412 Permit2 must not retry"
346 );
347 assert!(
348 !should_retry(StatusCode::BAD_GATEWAY),
349 "502 transport must not retry"
350 );
351 assert!(
352 !should_retry(StatusCode::TOO_MANY_REQUESTS),
353 "429 is facilitator /supported, not buyer retry"
354 );
355 assert!(!should_retry(StatusCode::OK), "200 must not retry");
356 }
357}