x402_paywall/
errors.rs

1//! The error response types for the paywall.
2
3use std::fmt::Display;
4
5use http::{HeaderName, HeaderValue, StatusCode};
6use x402_core::{
7    transport::{Accepts, PaymentRequired, PaymentResource},
8    types::{Base64EncodedHeader, Extension, Record, X402V2},
9};
10
11/// Represents an error response from the paywall.
12#[derive(Debug, Clone)]
13pub struct ErrorResponse {
14    /// The HTTP status code of the error response.
15    pub status: StatusCode,
16    /// The header to include in the error response.
17    pub header: ErrorResponseHeader,
18    /// The body of the error response.
19    ///
20    /// Body is **Boxed** to reduce size of the struct.
21    pub body: Box<PaymentRequired>,
22}
23
24impl ErrorResponse {
25    /// Payment needed to access resource
26    pub fn payment_required(
27        resource: PaymentResource,
28        accepts: Accepts,
29        extensions: Record<Extension>,
30    ) -> ErrorResponse {
31        let payment_required = PaymentRequired {
32            x402_version: X402V2,
33            error: "PAYMENT-SIGNATURE header is required".to_string(),
34            resource,
35            accepts,
36            extensions,
37        };
38
39        let header = Base64EncodedHeader::try_from(payment_required.clone()).unwrap_or(
40            Base64EncodedHeader("Failed to encode base64 PaymentRequired payload".to_string()),
41        );
42
43        ErrorResponse {
44            status: StatusCode::PAYMENT_REQUIRED,
45            header: ErrorResponseHeader::PaymentRequired(header),
46            body: Box::new(payment_required),
47        }
48    }
49
50    /// Malformed payment payload or requirements
51    pub fn invalid_payment(
52        reason: impl Display,
53        resource: PaymentResource,
54        accepts: Accepts,
55        extensions: Record<Extension>,
56    ) -> ErrorResponse {
57        let payment_required = PaymentRequired {
58            x402_version: X402V2,
59            error: reason.to_string(),
60            resource,
61            accepts,
62            extensions,
63        };
64
65        let header = Base64EncodedHeader::try_from(payment_required.clone()).unwrap_or(
66            Base64EncodedHeader("Failed to encode base64 PaymentRequired payload".to_string()),
67        );
68
69        ErrorResponse {
70            status: StatusCode::BAD_REQUEST,
71            header: ErrorResponseHeader::PaymentResponse(header),
72            body: Box::new(payment_required),
73        }
74    }
75
76    /// Payment verification or settlement failed
77    pub fn payment_failed(
78        reason: impl Display,
79        resource: PaymentResource,
80        accepts: Accepts,
81        extensions: Record<Extension>,
82    ) -> ErrorResponse {
83        let payment_required = PaymentRequired {
84            x402_version: X402V2,
85            error: reason.to_string(),
86            resource,
87            accepts,
88            extensions,
89        };
90
91        let header = Base64EncodedHeader::try_from(payment_required.clone()).unwrap_or(
92            Base64EncodedHeader("Failed to encode base64 PaymentRequired payload".to_string()),
93        );
94
95        ErrorResponse {
96            status: StatusCode::PAYMENT_REQUIRED,
97            header: ErrorResponseHeader::PaymentResponse(header),
98            body: Box::new(payment_required),
99        }
100    }
101
102    /// Internal server error during payment processing
103    pub fn server_error(
104        reason: impl Display,
105        resource: PaymentResource,
106        accepts: Accepts,
107        extensions: Record<Extension>,
108    ) -> ErrorResponse {
109        let payment_required = PaymentRequired {
110            x402_version: X402V2,
111            error: reason.to_string(),
112            resource,
113            accepts,
114            extensions,
115        };
116
117        let header = Base64EncodedHeader::try_from(payment_required.clone()).unwrap_or(
118            Base64EncodedHeader("Failed to encode base64 PaymentRequired payload".to_string()),
119        );
120
121        ErrorResponse {
122            status: StatusCode::INTERNAL_SERVER_ERROR,
123            header: ErrorResponseHeader::PaymentResponse(header),
124            body: Box::new(payment_required),
125        }
126    }
127}
128
129/// Represents the type of error header to include in a paywall error response.
130#[derive(Debug, Clone)]
131pub enum ErrorResponseHeader {
132    /// `PAYMENT-REQUIRED` header.
133    PaymentRequired(Base64EncodedHeader),
134    /// `PAYMENT-RESPONSE` header.
135    PaymentResponse(Base64EncodedHeader),
136}
137
138impl ErrorResponseHeader {
139    /// Get the header value to include in the response.
140    ///
141    /// Returns `None` if the header value could not be created.
142    pub fn header_value(self) -> Option<(HeaderName, HeaderValue)> {
143        match self {
144            ErrorResponseHeader::PaymentRequired(Base64EncodedHeader(s)) => {
145                HeaderValue::from_str(&s)
146                    .ok()
147                    .map(|v| (HeaderName::from_static("payment-required"), v))
148            }
149            ErrorResponseHeader::PaymentResponse(Base64EncodedHeader(s)) => {
150                HeaderValue::from_str(&s)
151                    .ok()
152                    .map(|v| (HeaderName::from_static("payment-response"), v))
153            }
154        }
155    }
156}
157
158#[cfg(feature = "axum")]
159impl axum::response::IntoResponse for ErrorResponse {
160    fn into_response(self) -> axum::response::Response {
161        let mut response = (self.status, axum::extract::Json(self.body)).into_response();
162        if let Some((name, val)) = self.header.header_value() {
163            response.headers_mut().insert(name, val);
164        }
165        response
166    }
167}