use axum_core::body::Body;
use axum_core::response::Response;
use compact_str::CompactString;
use http::header::CONTENT_TYPE;
use http::{HeaderValue, StatusCode};
use r402_protocol::error::{ErrorReason, FacilitatorError, FacilitatorTransportKind};
use r402_protocol::network::ChainId;
use r402_protocol::payment::{Base64Bytes, SettleResponse};
use r402_server::{FacilitatorSupportError, PaymentFlowName};
use serde_json::json;
use super::gate::Gate;
use crate::headers::{PAYMENT_REQUIRED, PAYMENT_RESPONSE, ensure_expose_headers, set_no_store};
#[derive(Debug, thiserror::Error)]
pub enum GateError {
#[error("Payment-Signature header is required")]
PaymentHeaderMissing,
#[error("Invalid or malformed payment header")]
InvalidPaymentHeader,
#[error("Unable to find matching payment requirements")]
NoPaymentMatching,
#[error("Verification failed: {message}")]
VerificationFailed {
reason: ErrorReason,
message: String,
},
#[error("settlement failed: {}", settlement_failure_summary(.0))]
Settlement(Box<SettleResponse>),
#[error("settlement aborted: {0}")]
SettlementAborted(String),
#[error("payment-required construction failed: {0}")]
PaymentRequiredBuild(String),
#[error("incompatible settlement mode {mode} with payment flow {flow}")]
IncompatibleSettlementMode {
mode: super::SettlementMode,
flow: PaymentFlowName,
},
#[error("missing scheme {scheme} on {network}")]
MissingScheme {
scheme: CompactString,
network: ChainId,
},
#[error("escrow scheme {scheme} is missing settle_on_cancel")]
MissingSettleOnCancel {
scheme: CompactString,
},
#[error(transparent)]
FacilitatorSupport(#[from] FacilitatorSupportError),
#[error("{kind}")]
Transport {
kind: FacilitatorTransportKind,
},
}
const fn support_parts(err: &FacilitatorSupportError) -> (&CompactString, &ChainId) {
match err {
FacilitatorSupportError::KindMissing { scheme, network }
| FacilitatorSupportError::MissingFeePayer { scheme, network }
| FacilitatorSupportError::InvalidFeePayer { scheme, network }
| FacilitatorSupportError::MissingReceiverAuthorizer { scheme, network }
| FacilitatorSupportError::ZeroReceiverAuthorizer { scheme, network }
| FacilitatorSupportError::InvalidReceiverAuthorizer { scheme, network } => {
(scheme, network)
}
}
}
fn settlement_failure_summary(resp: &SettleResponse) -> String {
match resp {
SettleResponse::Failure {
reason,
message,
network,
..
} => format!(
"{reason} ({network}){}",
message
.as_ref()
.map(|m| format!(": {m}"))
.unwrap_or_default(),
),
SettleResponse::Success { .. } => "success returned via error path".to_owned(),
_ => "unknown settlement variant".to_owned(),
}
}
#[must_use]
pub const fn reason_to_status(reason: &ErrorReason) -> StatusCode {
match reason {
ErrorReason::Permit2AllowanceRequired => StatusCode::PRECONDITION_FAILED,
_ => StatusCode::PAYMENT_REQUIRED,
}
}
impl GateError {
pub(crate) fn from_verify_facilitator(err: FacilitatorError) -> Self {
match err {
FacilitatorError::Transport { kind } => Self::Transport { kind },
other => {
let reason = other
.as_payment_problem()
.map_or(ErrorReason::UnexpectedVerifyError, |problem| {
problem.reason()
});
Self::VerificationFailed {
reason,
message: other.to_string(),
}
}
}
}
pub(crate) fn from_settle_facilitator(err: FacilitatorError) -> Self {
match err {
FacilitatorError::Transport { kind } => Self::Transport { kind },
other => Self::SettlementAborted(other.to_string()),
}
}
pub(crate) fn from_invalid_verify(reason: Option<ErrorReason>, message: Option<&str>) -> Self {
let reason = reason.unwrap_or(ErrorReason::UnexpectedVerifyError);
Self::VerificationFailed {
message: message.map_or_else(|| reason.to_string(), ToOwned::to_owned),
reason,
}
}
}
impl Gate {
#[must_use]
pub fn error_response(&self, err: GateError) -> Response {
match err {
GateError::PaymentHeaderMissing
| GateError::InvalidPaymentHeader
| GateError::NoPaymentMatching
| GateError::VerificationFailed { .. } => challenge_response(self, &err),
GateError::Settlement(failure) => settlement_failure_response(&failure),
GateError::PaymentRequiredBuild(ref detail) => json_status_response(
StatusCode::INTERNAL_SERVER_ERROR,
&json!({ "error": detail }),
),
GateError::IncompatibleSettlementMode { mode, flow } => json_status_response(
StatusCode::INTERNAL_SERVER_ERROR,
&json!({
"error": "incompatible settlement mode",
"mode": mode.as_str(),
"flow": flow.as_str(),
}),
),
GateError::MissingScheme {
ref scheme,
ref network,
} => json_status_response(
StatusCode::INTERNAL_SERVER_ERROR,
&json!({
"error": "missing scheme",
"scheme": scheme,
"network": network.to_string(),
}),
),
GateError::MissingSettleOnCancel { ref scheme } => json_status_response(
StatusCode::INTERNAL_SERVER_ERROR,
&json!({
"error": "missing settle_on_cancel",
"scheme": scheme,
}),
),
GateError::FacilitatorSupport(ref err) => {
let (scheme, network) = support_parts(err);
json_status_response(
StatusCode::INTERNAL_SERVER_ERROR,
&json!({
"error": "facilitator support",
"scheme": scheme,
"network": network.to_string(),
"reason": err.reason(),
}),
)
}
GateError::SettlementAborted(ref detail) => {
let mut response = json_status_response(
StatusCode::PAYMENT_REQUIRED,
&json!({
"error": "settlement aborted",
"details": detail,
}),
);
set_no_store(response.headers_mut());
response
}
GateError::Transport { kind } => json_status_response(
StatusCode::BAD_GATEWAY,
&json!({
"error": "facilitator transport",
"kind": kind.to_string(),
}),
),
}
}
}
fn challenge_response(gate: &Gate, err: &GateError) -> Response {
let Some(mut payment_required) = gate.payment_required().cloned() else {
return json_status_response(
StatusCode::INTERNAL_SERVER_ERROR,
&json!({ "error": "payment-required response has not been built" }),
);
};
let status = inferred_status(err);
payment_required.error = Some(err.to_string().into());
let Ok(body_bytes) = serde_json::to_vec(&payment_required) else {
return json_status_response(
StatusCode::INTERNAL_SERVER_ERROR,
&json!({ "error": "payment-required serialization failed" }),
);
};
let Ok(header_value) = HeaderValue::from_bytes(Base64Bytes::encode(&body_bytes).as_ref())
else {
return json_status_response(
StatusCode::INTERNAL_SERVER_ERROR,
&json!({ "error": "payment-required header encoding failed" }),
);
};
let mut response = Response::new(Body::from(body_bytes));
*response.status_mut() = status;
let _ = response
.headers_mut()
.insert(PAYMENT_REQUIRED, header_value);
let _ = response
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
ensure_expose_headers(response.headers_mut());
set_no_store(response.headers_mut());
response
}
fn settlement_failure_response(failure: &SettleResponse) -> Response {
let body_bytes = serde_json::to_vec(failure).unwrap_or_else(|_| b"{}".to_vec());
let header_value = failure
.encode_base64_any()
.and_then(|b64| HeaderValue::from_bytes(b64.as_ref()).ok());
let mut response = Response::new(Body::from(body_bytes));
*response.status_mut() = StatusCode::PAYMENT_REQUIRED;
let _ = response
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
if let Some(header_value) = header_value {
let _ = response
.headers_mut()
.insert(PAYMENT_RESPONSE, header_value);
}
ensure_expose_headers(response.headers_mut());
set_no_store(response.headers_mut());
response
}
pub(crate) fn json_status_response(status: StatusCode, body: &serde_json::Value) -> Response {
let mut response = Response::new(Body::from(body.to_string()));
*response.status_mut() = status;
let _ = response
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
ensure_expose_headers(response.headers_mut());
response
}
pub(crate) fn abort_response(status: StatusCode, body: Option<String>) -> Response {
let mut response = Response::new(Body::from(body.unwrap_or_default()));
*response.status_mut() = status;
let _ = response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
ensure_expose_headers(response.headers_mut());
response
}
const fn inferred_status(err: &GateError) -> StatusCode {
if let GateError::VerificationFailed { reason, .. } = err {
return reason_to_status(reason);
}
StatusCode::PAYMENT_REQUIRED
}