use http::{Extensions, HeaderMap, StatusCode};
use r402_core::ClientHooks;
use r402_core::client::{CreatedPayment, PaymentClient, PaymentResponseContext};
use r402_core::error::ClientError;
use r402_core::scheme::{FirstMatch, PaymentPolicy, PaymentSelector, SchemeClient};
use r402_core::wire;
use r402_core::wire::Base64Bytes;
use reqwest::{Client, Request, Response};
use reqwest_middleware as rqm;
#[cfg(feature = "telemetry")]
use tracing::{debug, info, instrument, trace};
#[allow(
missing_debug_implementations,
reason = "PaymentClient contains dyn trait objects"
)]
pub struct X402Client<TSelector = FirstMatch> {
inner: PaymentClient<TSelector>,
}
impl X402Client<FirstMatch> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl Default for X402Client<FirstMatch> {
fn default() -> Self {
Self {
inner: PaymentClient::new(),
}
}
}
impl<TSelector> X402Client<TSelector> {
#[must_use]
pub const fn from_payment_client(inner: PaymentClient<TSelector>) -> Self {
Self { inner }
}
#[must_use]
pub const fn payment_client(&self) -> &PaymentClient<TSelector> {
&self.inner
}
#[must_use]
pub fn register<S>(mut self, scheme: S) -> Self
where
S: SchemeClient + 'static,
{
self.inner = self.inner.register(scheme);
self
}
#[must_use]
pub fn with_selector<P: PaymentSelector>(self, selector: P) -> X402Client<P> {
X402Client {
inner: self.inner.with_selector(selector),
}
}
#[must_use]
pub fn with_policy<P: PaymentPolicy + 'static>(mut self, policy: P) -> Self {
self.inner = self.inner.with_policy(policy);
self
}
#[must_use]
pub fn with_hook(mut self, hook: impl ClientHooks + 'static) -> Self {
self.inner = self.inner.with_hook(hook);
self
}
}
impl<TSelector> X402Client<TSelector>
where
TSelector: PaymentSelector,
{
#[cfg_attr(
feature = "telemetry",
instrument(name = "x402.reqwest.make_payment_headers", skip_all, err)
)]
pub async fn make_payment_headers(&self, res: Response) -> Result<HeaderMap, ClientError> {
let payment_required = parse_payment_required(res)
.await
.ok_or_else(|| ClientError::Parse("Invalid 402 response".to_owned()))?;
let created = self.inner.create_payment(&payment_required).await?;
Ok(payment_signature_headers(&created))
}
pub async fn create_payment(
&self,
payment_required: &wire::PaymentRequired,
) -> Result<CreatedPayment, ClientError> {
self.inner.create_payment(payment_required).await
}
}
#[must_use]
pub fn payment_signature_headers(created: &CreatedPayment) -> HeaderMap {
let mut headers = HeaderMap::new();
#[allow(
clippy::expect_used,
reason = "base64-encoded payload is always valid ASCII header"
)]
headers.insert(
"Payment-Signature",
created
.signed_payload
.parse()
.expect("signed payload is valid header value"),
);
headers
}
#[cfg_attr(
feature = "telemetry",
instrument(name = "x402.reqwest.next", skip_all)
)]
async fn run_next(
next: rqm::Next<'_>,
req: Request,
extensions: &mut Extensions,
) -> rqm::Result<Response> {
next.run(req, extensions).await
}
#[async_trait::async_trait]
impl<TSelector> rqm::Middleware for X402Client<TSelector>
where
TSelector: PaymentSelector + Send + Sync + 'static,
{
#[cfg_attr(
feature = "telemetry",
instrument(name = "x402.reqwest.handle", skip_all, err)
)]
async fn handle(
&self,
req: Request,
extensions: &mut Extensions,
next: rqm::Next<'_>,
) -> rqm::Result<Response> {
let retry_template = req.try_clone();
let res = run_next(next.clone(), req, extensions).await?;
if res.status() != StatusCode::PAYMENT_REQUIRED {
#[cfg(feature = "telemetry")]
trace!(status = ?res.status(), "No payment required, returning response");
return Ok(res);
}
#[cfg(feature = "telemetry")]
info!(url = ?res.url(), "Received 402 Payment Required, processing payment");
let Some(template) = retry_template else {
#[cfg(feature = "telemetry")]
tracing::warn!("Cannot auto-retry 402: request body not cloneable, returning raw 402");
return Ok(res);
};
let payment_required = parse_payment_required(res).await.ok_or_else(|| {
rqm::Error::Middleware(ClientError::Parse("Invalid 402 response".into()).into())
})?;
let created = self
.inner
.create_payment(&payment_required)
.await
.map_err(|e| rqm::Error::Middleware(e.into()))?;
let paid = clone_with_payment(&template, &created)
.map_err(|e| rqm::Error::Middleware(e.into()))?;
#[cfg(feature = "telemetry")]
trace!(url = ?paid.url(), "Retrying request with payment headers");
let response = run_next(next.clone(), paid, extensions).await?;
let Some(recovered) = self
.dispatch_and_maybe_recover(&payment_required, &created, &response)
.await
.map_err(|e| rqm::Error::Middleware(e.into()))?
else {
return Ok(response);
};
let second = clone_with_payment(&template, &recovered)
.map_err(|e| rqm::Error::Middleware(e.into()))?;
let second_response = run_next(next, second, extensions).await?;
let ctx = build_response_context(&payment_required, &recovered, &second_response);
if ctx.settle_response.is_some() || ctx.corrective_payment_required.is_some() {
let _ = self.inner.handle_payment_response(&ctx).await;
}
Ok(second_response)
}
}
impl<TSelector: PaymentSelector> X402Client<TSelector> {
async fn dispatch_and_maybe_recover(
&self,
original: &wire::PaymentRequired,
created: &CreatedPayment,
response: &Response,
) -> Result<Option<CreatedPayment>, ClientError> {
let ctx = build_response_context(original, created, response);
if ctx.settle_response.is_none() && ctx.corrective_payment_required.is_none() {
return Ok(None);
}
let result = self.inner.handle_payment_response(&ctx).await;
if !result.recovered {
return Ok(None);
}
let challenge = ctx.corrective_payment_required.as_ref().unwrap_or(original);
let fresh = self.inner.create_payment(challenge).await?;
Ok(Some(fresh))
}
}
fn build_response_context(
original: &wire::PaymentRequired,
created: &CreatedPayment,
response: &Response,
) -> PaymentResponseContext {
let settle_response = response
.headers()
.get("Payment-Response")
.and_then(|h| Base64Bytes::from(h.as_bytes()).decode().ok())
.and_then(|b| serde_json::from_slice(&b).ok());
let corrective_payment_required =
if settle_response.is_none() && response.status() == StatusCode::PAYMENT_REQUIRED {
response
.headers()
.get("Payment-Required")
.and_then(|h| Base64Bytes::from(h.as_bytes()).decode().ok())
.and_then(|b| serde_json::from_slice(&b).ok())
} else {
None
};
let mut ctx = PaymentResponseContext::new(original.clone(), created.signed_payload.clone());
if let Some(settle) = settle_response {
ctx = ctx.with_settle_response(settle);
}
if let Some(required) = corrective_payment_required {
ctx = ctx.with_corrective_payment_required(required);
}
ctx
}
fn clone_with_payment(
template: &Request,
created: &CreatedPayment,
) -> Result<Request, ClientError> {
let mut req = template
.try_clone()
.ok_or(ClientError::RequestNotCloneable)?;
req.headers_mut().extend(payment_signature_headers(created));
Ok(req)
}
#[cfg_attr(
feature = "telemetry",
instrument(name = "x402.reqwest.parse_payment_required", skip(response))
)]
pub async fn parse_payment_required(response: Response) -> Option<wire::PaymentRequired> {
let v2_from_header = response
.headers()
.get("Payment-Required")
.and_then(|h| Base64Bytes::from(h.as_bytes()).decode().ok())
.and_then(|b| serde_json::from_slice::<wire::PaymentRequired>(&b).ok());
if let Some(v2_payment_required) = v2_from_header {
#[cfg(feature = "telemetry")]
debug!("Parsed V2 payment required from header");
return Some(v2_payment_required);
}
if let Ok(body_bytes) = response.bytes().await
&& let Ok(v2_from_body) = serde_json::from_slice::<wire::PaymentRequired>(&body_bytes)
{
#[cfg(feature = "telemetry")]
debug!("Parsed V2 payment required from response body");
return Some(v2_from_body);
}
#[cfg(feature = "telemetry")]
debug!("Could not parse payment required from response");
None
}
impl<S> X402Client<S>
where
Self: rqm::Middleware,
{
#[must_use]
pub fn wrap(self, client: Client) -> rqm::ClientWithMiddleware {
rqm::ClientBuilder::new(client).with(self).build()
}
#[must_use]
pub fn wrap_builder(self, client: Client) -> rqm::ClientBuilder {
rqm::ClientBuilder::new(client).with(self)
}
}
pub trait WithPayments {
fn with_payments<S>(self, x402: X402Client<S>) -> rqm::ClientWithMiddleware
where
X402Client<S>: rqm::Middleware;
}
impl WithPayments for Client {
fn with_payments<S>(self, x402: X402Client<S>) -> rqm::ClientWithMiddleware
where
X402Client<S>: rqm::Middleware,
{
x402.wrap(self)
}
}