1use 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#[derive(Debug, thiserror::Error)]
19pub enum GateError {
20 #[error("Payment-Signature header is required")]
22 PaymentHeaderMissing,
23 #[error("Invalid or malformed payment header")]
25 InvalidPaymentHeader,
26 #[error("Unable to find matching payment requirements")]
28 NoPaymentMatching,
29 #[error("Verification failed: {message}")]
31 VerificationFailed {
32 reason: ErrorReason,
34 message: String,
36 },
37 #[error("settlement failed: {}", settlement_failure_summary(.0))]
39 Settlement(Box<SettleResponse>),
40 #[error("settlement aborted: {0}")]
42 SettlementAborted(String),
43 #[error("payment-required construction failed: {0}")]
45 PaymentRequiredBuild(String),
46 #[error("incompatible settlement mode {mode} with payment flow {flow}")]
48 IncompatibleSettlementMode {
49 mode: super::SettlementMode,
51 flow: PaymentFlowName,
53 },
54 #[error("missing scheme {scheme} on {network}")]
56 MissingScheme {
57 scheme: CompactString,
59 network: ChainId,
61 },
62 #[error("escrow scheme {scheme} is missing settle_on_cancel")]
64 MissingSettleOnCancel {
65 scheme: CompactString,
67 },
68 #[error(transparent)]
70 FacilitatorSupport(#[from] FacilitatorSupportError),
71 #[error("{kind}")]
73 Transport {
74 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#[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 #[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}