Skip to main content

r402_http/server/
fail.rs

1//! Gate errors and HTTP mapping (402 / 412 / 500 / 502).
2
3use axum_core::body::Body;
4use axum_core::response::Response;
5use compact_str::CompactString;
6use http::header::CONTENT_TYPE;
7use http::{HeaderValue, StatusCode};
8use r402_protocol::error::{ErrorReason, FacilitatorError, FacilitatorTransportKind};
9use r402_protocol::network::ChainId;
10use r402_protocol::payment::{Base64Bytes, SettleResponse};
11use r402_server::{FacilitatorSupportError, PaymentFlowName};
12use serde_json::json;
13
14use super::gate::Gate;
15use crate::headers::{PAYMENT_REQUIRED, PAYMENT_RESPONSE, ensure_expose_headers, set_no_store};
16
17/// HTTP payment-gate failure.
18#[derive(Debug, thiserror::Error)]
19pub enum GateError {
20    /// `Payment-Signature` is absent.
21    #[error("Payment-Signature header is required")]
22    PaymentHeaderMissing,
23    /// `Payment-Signature` is present but not valid base64 JSON.
24    #[error("Invalid or malformed payment header")]
25    InvalidPaymentHeader,
26    /// No accept matches the payload.
27    #[error("Unable to find matching payment requirements")]
28    NoPaymentMatching,
29    /// Facilitator rejected the payment (`isValid: false` or verify error).
30    #[error("Verification failed: {message}")]
31    VerificationFailed {
32        /// Wire reason used for 402 vs 412.
33        reason: ErrorReason,
34        /// Display text copied onto `PaymentRequired.error`.
35        message: String,
36    },
37    /// Structured settle failure. Emitted as 402 + `Payment-Response`.
38    #[error("settlement failed: {}", settlement_failure_summary(.0))]
39    Settlement(Box<SettleResponse>),
40    /// Settle never produced a structured body.
41    #[error("settlement aborted: {0}")]
42    SettlementAborted(String),
43    /// 402 body construction failed.
44    #[error("payment-required construction failed: {0}")]
45    PaymentRequiredBuild(String),
46    /// Concurrent/Background used with upfront or escrow.
47    #[error("incompatible settlement mode {mode} with payment flow {flow}")]
48    IncompatibleSettlementMode {
49        /// Configured HTTP settlement mode.
50        mode: super::SettlementMode,
51        /// Offending payment flow.
52        flow: PaymentFlowName,
53    },
54    /// No scheme registered for this accept.
55    #[error("missing scheme {scheme} on {network}")]
56    MissingScheme {
57        /// Wire scheme name.
58        scheme: CompactString,
59        /// Accept network.
60        network: ChainId,
61    },
62    /// Escrow accept whose scheme does not implement `settle_on_cancel`.
63    #[error("escrow scheme {scheme} is missing settle_on_cancel")]
64    MissingSettleOnCancel {
65        /// Wire scheme name.
66        scheme: CompactString,
67    },
68    /// Facilitator `/supported` kind missing or extra unusable. HTTP 500.
69    #[error(transparent)]
70    FacilitatorSupport(#[from] FacilitatorSupportError),
71    /// Facilitator transport failure. HTTP 502.
72    #[error("{kind}")]
73    Transport {
74        /// Transport failure kind.
75        kind: FacilitatorTransportKind,
76    },
77}
78
79const fn support_parts(err: &FacilitatorSupportError) -> (&CompactString, &ChainId) {
80    match err {
81        FacilitatorSupportError::KindMissing { scheme, network }
82        | FacilitatorSupportError::MissingFeePayer { scheme, network }
83        | FacilitatorSupportError::InvalidFeePayer { scheme, network }
84        | FacilitatorSupportError::MissingReceiverAuthorizer { scheme, network }
85        | FacilitatorSupportError::ZeroReceiverAuthorizer { scheme, network }
86        | FacilitatorSupportError::InvalidReceiverAuthorizer { scheme, network } => {
87            (scheme, network)
88        }
89    }
90}
91
92fn settlement_failure_summary(resp: &SettleResponse) -> String {
93    match resp {
94        SettleResponse::Failure {
95            reason,
96            message,
97            network,
98            ..
99        } => format!(
100            "{reason} ({network}){}",
101            message
102                .as_ref()
103                .map(|m| format!(": {m}"))
104                .unwrap_or_default(),
105        ),
106        SettleResponse::Success { .. } => "success returned via error path".to_owned(),
107        _ => "unknown settlement variant".to_owned(),
108    }
109}
110
111/// Maps an [`ErrorReason`] to 412 (Permit2) or 402.
112#[must_use]
113pub const fn reason_to_status(reason: &ErrorReason) -> StatusCode {
114    match reason {
115        ErrorReason::Permit2AllowanceRequired => StatusCode::PRECONDITION_FAILED,
116        _ => StatusCode::PAYMENT_REQUIRED,
117    }
118}
119
120impl GateError {
121    pub(crate) fn from_verify_facilitator(err: FacilitatorError) -> Self {
122        match err {
123            FacilitatorError::Transport { kind } => Self::Transport { kind },
124            other => {
125                let reason = other
126                    .as_payment_problem()
127                    .map_or(ErrorReason::UnexpectedVerifyError, |problem| {
128                        problem.reason()
129                    });
130                Self::VerificationFailed {
131                    reason,
132                    message: other.to_string(),
133                }
134            }
135        }
136    }
137
138    pub(crate) fn from_settle_facilitator(err: FacilitatorError) -> Self {
139        match err {
140            FacilitatorError::Transport { kind } => Self::Transport { kind },
141            other => Self::SettlementAborted(other.to_string()),
142        }
143    }
144
145    pub(crate) fn from_invalid_verify(reason: Option<ErrorReason>, message: Option<&str>) -> Self {
146        let reason = reason.unwrap_or(ErrorReason::UnexpectedVerifyError);
147        Self::VerificationFailed {
148            message: message.map_or_else(|| reason.to_string(), ToOwned::to_owned),
149            reason,
150        }
151    }
152}
153
154impl Gate {
155    /// Converts a [`GateError`] into an HTTP response.
156    #[must_use]
157    pub fn error_response(&self, err: GateError) -> Response {
158        match err {
159            GateError::PaymentHeaderMissing
160            | GateError::InvalidPaymentHeader
161            | GateError::NoPaymentMatching
162            | GateError::VerificationFailed { .. } => challenge_response(self, &err),
163            GateError::Settlement(failure) => settlement_failure_response(&failure),
164            GateError::PaymentRequiredBuild(ref detail) => json_status_response(
165                StatusCode::INTERNAL_SERVER_ERROR,
166                &json!({ "error": detail }),
167            ),
168            GateError::IncompatibleSettlementMode { mode, flow } => json_status_response(
169                StatusCode::INTERNAL_SERVER_ERROR,
170                &json!({
171                    "error": "incompatible settlement mode",
172                    "mode": mode.as_str(),
173                    "flow": flow.as_str(),
174                }),
175            ),
176            GateError::MissingScheme {
177                ref scheme,
178                ref network,
179            } => json_status_response(
180                StatusCode::INTERNAL_SERVER_ERROR,
181                &json!({
182                    "error": "missing scheme",
183                    "scheme": scheme,
184                    "network": network.to_string(),
185                }),
186            ),
187            GateError::MissingSettleOnCancel { ref scheme } => json_status_response(
188                StatusCode::INTERNAL_SERVER_ERROR,
189                &json!({
190                    "error": "missing settle_on_cancel",
191                    "scheme": scheme,
192                }),
193            ),
194            GateError::FacilitatorSupport(ref err) => {
195                let (scheme, network) = support_parts(err);
196                json_status_response(
197                    StatusCode::INTERNAL_SERVER_ERROR,
198                    &json!({
199                        "error": "facilitator support",
200                        "scheme": scheme,
201                        "network": network.to_string(),
202                        "reason": err.reason(),
203                    }),
204                )
205            }
206            GateError::SettlementAborted(ref detail) => {
207                let mut response = json_status_response(
208                    StatusCode::PAYMENT_REQUIRED,
209                    &json!({
210                        "error": "settlement aborted",
211                        "details": detail,
212                    }),
213                );
214                set_no_store(response.headers_mut());
215                response
216            }
217            GateError::Transport { kind } => json_status_response(
218                StatusCode::BAD_GATEWAY,
219                &json!({
220                    "error": "facilitator transport",
221                    "kind": kind.to_string(),
222                }),
223            ),
224        }
225    }
226}
227
228fn challenge_response(gate: &Gate, err: &GateError) -> Response {
229    let Some(mut payment_required) = gate.payment_required().cloned() else {
230        return json_status_response(
231            StatusCode::INTERNAL_SERVER_ERROR,
232            &json!({ "error": "payment-required response has not been built" }),
233        );
234    };
235    let status = inferred_status(err);
236    payment_required.error = Some(err.to_string().into());
237    let Ok(body_bytes) = serde_json::to_vec(&payment_required) else {
238        return json_status_response(
239            StatusCode::INTERNAL_SERVER_ERROR,
240            &json!({ "error": "payment-required serialization failed" }),
241        );
242    };
243    let Ok(header_value) = HeaderValue::from_bytes(Base64Bytes::encode(&body_bytes).as_ref())
244    else {
245        return json_status_response(
246            StatusCode::INTERNAL_SERVER_ERROR,
247            &json!({ "error": "payment-required header encoding failed" }),
248        );
249    };
250    let mut response = Response::new(Body::from(body_bytes));
251    *response.status_mut() = status;
252    let _ = response
253        .headers_mut()
254        .insert(PAYMENT_REQUIRED, header_value);
255    let _ = response
256        .headers_mut()
257        .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
258    ensure_expose_headers(response.headers_mut());
259    set_no_store(response.headers_mut());
260    response
261}
262
263fn settlement_failure_response(failure: &SettleResponse) -> Response {
264    let body_bytes = serde_json::to_vec(failure).unwrap_or_else(|_| b"{}".to_vec());
265    let header_value = failure
266        .encode_base64_any()
267        .and_then(|b64| HeaderValue::from_bytes(b64.as_ref()).ok());
268    let mut response = Response::new(Body::from(body_bytes));
269    *response.status_mut() = StatusCode::PAYMENT_REQUIRED;
270    let _ = response
271        .headers_mut()
272        .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
273    if let Some(header_value) = header_value {
274        let _ = response
275            .headers_mut()
276            .insert(PAYMENT_RESPONSE, header_value);
277    }
278    ensure_expose_headers(response.headers_mut());
279    set_no_store(response.headers_mut());
280    response
281}
282
283pub(crate) fn json_status_response(status: StatusCode, body: &serde_json::Value) -> Response {
284    let mut response = Response::new(Body::from(body.to_string()));
285    *response.status_mut() = status;
286    let _ = response
287        .headers_mut()
288        .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
289    ensure_expose_headers(response.headers_mut());
290    response
291}
292
293pub(crate) fn abort_response(status: StatusCode, body: Option<String>) -> Response {
294    let mut response = Response::new(Body::from(body.unwrap_or_default()));
295    *response.status_mut() = status;
296    let _ = response.headers_mut().insert(
297        CONTENT_TYPE,
298        HeaderValue::from_static("text/plain; charset=utf-8"),
299    );
300    ensure_expose_headers(response.headers_mut());
301    response
302}
303
304const fn inferred_status(err: &GateError) -> StatusCode {
305    if let GateError::VerificationFailed { reason, .. } = err {
306        return reason_to_status(reason);
307    }
308    StatusCode::PAYMENT_REQUIRED
309}