Skip to main content

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 Display for ErrorResponse {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.write_str("payment required")
27    }
28}
29
30impl ErrorResponse {
31    /// Payment needed to access resource
32    pub fn payment_required(
33        resource: PaymentResource,
34        accepts: Accepts,
35        extensions: Record<Extension>,
36    ) -> ErrorResponse {
37        let payment_required = PaymentRequired {
38            x402_version: X402V2,
39            error: "PAYMENT-SIGNATURE header is required".to_string(),
40            resource,
41            accepts,
42            extensions,
43        };
44
45        let header = Base64EncodedHeader::try_from(payment_required.clone()).unwrap_or(
46            Base64EncodedHeader("Failed to encode base64 PaymentRequired payload".to_string()),
47        );
48
49        ErrorResponse {
50            status: StatusCode::PAYMENT_REQUIRED,
51            header: ErrorResponseHeader::PaymentRequired(header),
52            body: Box::new(payment_required),
53        }
54    }
55
56    /// Malformed payment payload or requirements
57    pub fn invalid_payment(
58        reason: impl Display,
59        resource: PaymentResource,
60        accepts: Accepts,
61        extensions: Record<Extension>,
62    ) -> ErrorResponse {
63        let payment_required = PaymentRequired {
64            x402_version: X402V2,
65            error: reason.to_string(),
66            resource,
67            accepts,
68            extensions,
69        };
70
71        let header = Base64EncodedHeader::try_from(payment_required.clone()).unwrap_or(
72            Base64EncodedHeader("Failed to encode base64 PaymentRequired payload".to_string()),
73        );
74
75        ErrorResponse {
76            status: StatusCode::BAD_REQUEST,
77            header: ErrorResponseHeader::PaymentResponse(header),
78            body: Box::new(payment_required),
79        }
80    }
81
82    /// Payment verification or settlement failed
83    pub fn payment_failed(
84        reason: impl Display,
85        resource: PaymentResource,
86        accepts: Accepts,
87        extensions: Record<Extension>,
88    ) -> ErrorResponse {
89        let payment_required = PaymentRequired {
90            x402_version: X402V2,
91            error: reason.to_string(),
92            resource,
93            accepts,
94            extensions,
95        };
96
97        let header = Base64EncodedHeader::try_from(payment_required.clone()).unwrap_or(
98            Base64EncodedHeader("Failed to encode base64 PaymentRequired payload".to_string()),
99        );
100
101        ErrorResponse {
102            status: StatusCode::PAYMENT_REQUIRED,
103            header: ErrorResponseHeader::PaymentResponse(header),
104            body: Box::new(payment_required),
105        }
106    }
107
108    /// Internal server error during payment processing
109    pub fn server_error(
110        reason: impl Display,
111        resource: PaymentResource,
112        accepts: Accepts,
113        extensions: Record<Extension>,
114    ) -> ErrorResponse {
115        let payment_required = PaymentRequired {
116            x402_version: X402V2,
117            error: reason.to_string(),
118            resource,
119            accepts,
120            extensions,
121        };
122
123        let header = Base64EncodedHeader::try_from(payment_required.clone()).unwrap_or(
124            Base64EncodedHeader("Failed to encode base64 PaymentRequired payload".to_string()),
125        );
126
127        ErrorResponse {
128            status: StatusCode::INTERNAL_SERVER_ERROR,
129            header: ErrorResponseHeader::PaymentResponse(header),
130            body: Box::new(payment_required),
131        }
132    }
133}
134
135/// Represents the type of error header to include in a paywall error response.
136#[derive(Debug, Clone)]
137pub enum ErrorResponseHeader {
138    /// `PAYMENT-REQUIRED` header.
139    PaymentRequired(Base64EncodedHeader),
140    /// `PAYMENT-RESPONSE` header.
141    PaymentResponse(Base64EncodedHeader),
142}
143
144impl ErrorResponseHeader {
145    /// Get the header value to include in the response.
146    ///
147    /// Returns `None` if the header value could not be created.
148    pub fn header_value(self) -> Option<(HeaderName, HeaderValue)> {
149        match self {
150            ErrorResponseHeader::PaymentRequired(Base64EncodedHeader(s)) => {
151                HeaderValue::from_str(&s)
152                    .ok()
153                    .map(|v| (HeaderName::from_static("payment-required"), v))
154            }
155            ErrorResponseHeader::PaymentResponse(Base64EncodedHeader(s)) => {
156                HeaderValue::from_str(&s)
157                    .ok()
158                    .map(|v| (HeaderName::from_static("payment-response"), v))
159            }
160        }
161    }
162}
163
164#[cfg(feature = "axum")]
165impl axum::response::IntoResponse for ErrorResponse {
166    fn into_response(self) -> axum::response::Response {
167        let mut response = (self.status, axum::extract::Json(self.body)).into_response();
168        if let Some((name, val)) = self.header.header_value() {
169            response.headers_mut().insert(name, val);
170        }
171        response
172    }
173}
174
175#[cfg(feature = "actix-web")]
176impl ErrorResponse {
177    fn actix_header(&self) -> (&'static str, &str) {
178        match &self.header {
179            ErrorResponseHeader::PaymentRequired(base64_encoded_header) => {
180                ("payment-required", &base64_encoded_header.0)
181            }
182            ErrorResponseHeader::PaymentResponse(base64_encoded_header) => {
183                ("payment-response", &base64_encoded_header.0)
184            }
185        }
186    }
187}
188
189#[cfg(feature = "actix-web")]
190impl actix_web::ResponseError for ErrorResponse {
191    fn status_code(&self) -> actix_web::http::StatusCode {
192        actix_web::http::StatusCode::from_u16(self.status.as_u16()).unwrap()
193    }
194
195    fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
196        actix_web::HttpResponseBuilder::new(self.status_code())
197            .insert_header(self.actix_header())
198            .json(&self.body)
199    }
200}