tap_mcp_bridge/error.rs
1//! Error types for the TAP-MCP Bridge.
2//!
3//! This module defines all error types that can occur during TAP-MCP bridge operations.
4//! All errors implement the standard [`std::error::Error`] trait via [`thiserror::Error`].
5//!
6//! # Error Categories
7//!
8//! - **Signature Errors** ([`BridgeError::SignatureError`], [`BridgeError::CryptoError`]):
9//! Cryptographic operation failures
10//! - **Network Errors** ([`BridgeError::HttpError`]): HTTP communication failures
11//! - **Validation Errors** ([`BridgeError::InvalidMerchantUrl`]): Input validation failures
12//! - **Protocol Errors** ([`BridgeError::MerchantError`]): TAP protocol violations
13//!
14//! # Examples
15//!
16//! ```
17//! use tap_mcp_bridge::error::{BridgeError, Result};
18//!
19//! fn validate_url(url: &str) -> Result<String> {
20//! if !url.starts_with("https://") {
21//! return Err(BridgeError::InvalidMerchantUrl("URL must use HTTPS".to_string()));
22//! }
23//! Ok(url.to_string())
24//! }
25//! ```
26
27use thiserror::Error;
28
29/// Result type alias for bridge operations.
30///
31/// This is a convenience type that uses [`BridgeError`] as the error type.
32/// All fallible functions in this crate return this type.
33///
34/// Results should be handled by the caller - either checked for errors,
35/// propagated with `?`, or explicitly acknowledged with `.unwrap()` or
36/// `.expect()` in cases where failure is impossible.
37pub type Result<T> = std::result::Result<T, BridgeError>;
38
39/// Errors that can occur in the TAP-MCP bridge.
40///
41/// All variants include contextual information about what went wrong.
42/// The error messages are designed to be user-facing and actionable.
43///
44/// # Error Recovery
45///
46/// - **Transient errors** ([`HttpError`](Self::HttpError)): Retry with exponential backoff
47/// - **Validation errors** ([`InvalidMerchantUrl`](Self::InvalidMerchantUrl)): Fix input and retry
48/// - **Cryptographic errors** ([`SignatureError`](Self::SignatureError),
49/// [`CryptoError`](Self::CryptoError)): Check key configuration
50/// - **Protocol errors** ([`MerchantError`](Self::MerchantError)): Contact merchant support
51///
52/// This type implements `#[must_use]` to ensure errors are not silently ignored.
53/// Always handle errors by checking, propagating, or explicitly panicking.
54#[must_use = "errors should be handled, propagated, or explicitly panicked"]
55#[derive(Debug, Error)]
56pub enum BridgeError {
57 /// TAP signature generation failed.
58 ///
59 /// This error occurs when the bridge cannot generate a valid RFC 9421 HTTP Message Signature.
60 /// Common causes include:
61 /// - Invalid signing key format
62 /// - System time errors (cannot determine signature timestamp)
63 /// - Signature base string construction failures
64 ///
65 /// # Recovery
66 ///
67 /// Check that the Ed25519 signing key is valid and system time is correctly set.
68 #[error("TAP signature generation failed: {0}")]
69 SignatureError(String),
70
71 /// HTTP request failed.
72 ///
73 /// This error wraps [`reqwest::Error`] and occurs when network communication with
74 /// the merchant fails. Common causes include:
75 /// - Network timeouts (default: 30 seconds)
76 /// - Connection refused (merchant server down)
77 /// - DNS resolution failures
78 /// - TLS/SSL errors
79 ///
80 /// # Recovery
81 ///
82 /// Retry the request with exponential backoff. If the error persists, verify:
83 /// - Merchant URL is correct and accessible
84 /// - Network connectivity is available
85 /// - Firewall/proxy settings allow HTTPS connections
86 #[error("HTTP request failed: {0}")]
87 HttpError(#[from] reqwest::Error),
88
89 /// Invalid merchant response.
90 ///
91 /// This error occurs when the merchant returns a response that violates the TAP protocol.
92 /// Common causes include:
93 /// - Unexpected HTTP status code
94 /// - Malformed JSON response
95 /// - Missing required fields in response
96 /// - Protocol version mismatch
97 ///
98 /// # Recovery
99 ///
100 /// This usually indicates a merchant-side issue. Contact the merchant to verify:
101 /// - Their TAP implementation is up to date
102 /// - The endpoint accepts the request format being sent
103 /// - Any API keys or authentication tokens are valid
104 #[error("Invalid merchant response: {0}")]
105 MerchantError(String),
106
107 /// Cryptographic operation failed.
108 ///
109 /// This error occurs when a low-level cryptographic operation fails.
110 /// Common causes include:
111 /// - Invalid key material
112 /// - Hash computation failures
113 /// - Base64 encoding/decoding errors
114 ///
115 /// # Recovery
116 ///
117 /// Verify that:
118 /// - Signing keys are valid Ed25519 keys
119 /// - Key material has not been corrupted
120 /// - System has sufficient entropy for cryptographic operations
121 #[error("Cryptographic operation failed: {0}")]
122 CryptoError(String),
123
124 /// Invalid merchant URL.
125 ///
126 /// This error occurs when input validation rejects a merchant URL.
127 /// Common causes include:
128 /// - Non-HTTPS URL (HTTP is not allowed)
129 /// - Localhost or loopback addresses (security restriction)
130 /// - Malformed URL syntax
131 ///
132 /// # Recovery
133 ///
134 /// Ensure the merchant URL:
135 /// - Uses HTTPS scheme (`https://`)
136 /// - Is not a localhost address (`localhost`, `127.0.0.1`)
137 /// - Has valid syntax per [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986)
138 ///
139 /// # Examples
140 ///
141 /// ```
142 /// use tap_mcp_bridge::error::BridgeError;
143 ///
144 /// // These URLs will be rejected:
145 /// let err = BridgeError::InvalidMerchantUrl("http://example.com".to_string());
146 /// assert!(err.to_string().contains("Invalid merchant URL"));
147 ///
148 /// let err = BridgeError::InvalidMerchantUrl("https://localhost/checkout".to_string());
149 /// assert!(err.to_string().contains("Invalid merchant URL"));
150 /// ```
151 #[error("Invalid merchant URL: {0}")]
152 InvalidMerchantUrl(String),
153
154 /// Invalid consumer ID.
155 ///
156 /// This error occurs when input validation rejects a consumer ID.
157 /// Consumer IDs must meet these requirements:
158 /// - Not empty
159 /// - Maximum 64 characters
160 /// - Only alphanumeric characters, hyphens, and underscores
161 ///
162 /// # Recovery
163 ///
164 /// Ensure the consumer ID:
165 /// - Contains only letters (a-z, A-Z), numbers (0-9), hyphens (-), and underscores (_)
166 /// - Has at least 1 character
167 /// - Has no more than 64 characters
168 ///
169 /// # Examples
170 ///
171 /// ```
172 /// use tap_mcp_bridge::error::BridgeError;
173 ///
174 /// // These consumer IDs will be rejected:
175 /// let err = BridgeError::InvalidConsumerId("consumer@example.com".to_string());
176 /// assert!(err.to_string().contains("Invalid consumer ID"));
177 ///
178 /// let err = BridgeError::InvalidConsumerId("".to_string());
179 /// assert!(err.to_string().contains("Invalid consumer ID"));
180 /// ```
181 #[error("Invalid consumer ID: {0}")]
182 InvalidConsumerId(String),
183
184 /// Replay attack detected.
185 ///
186 /// This error occurs when a request with a previously seen nonce is received
187 /// within the 8-minute validity window.
188 #[error("Replay attack detected")]
189 ReplayAttack,
190
191 /// Request too old.
192 ///
193 /// This error occurs when the request timestamp is outside the allowed window.
194 #[error("Request too old (timestamp: {0:?})")]
195 RequestTooOld(std::time::SystemTime),
196
197 /// Rate limit exceeded.
198 ///
199 /// This error occurs when too many requests are made in a short period.
200 /// Rate limiting protects against `DoS` attacks and ensures fair resource usage.
201 ///
202 /// # Recovery
203 ///
204 /// Wait a short period (e.g., 1 second) and retry the request.
205 /// Consider implementing exponential backoff for repeated failures.
206 #[error("Rate limit exceeded, try again later")]
207 RateLimitExceeded,
208
209 /// Circuit breaker is open.
210 ///
211 /// This error occurs when the circuit breaker has detected too many failures
212 /// and is temporarily rejecting all requests to prevent cascading failures.
213 /// The circuit will automatically transition to half-open state after the
214 /// reset timeout expires.
215 ///
216 /// # Recovery
217 ///
218 /// Wait for the circuit breaker's reset timeout to expire (typically 60 seconds).
219 /// The circuit will then allow limited test requests to determine if the
220 /// underlying service has recovered.
221 #[error("Circuit breaker is open")]
222 CircuitOpen,
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_error_display() {
231 let error = BridgeError::SignatureError("test error".into());
232 assert_eq!(error.to_string(), "TAP signature generation failed: test error");
233 }
234
235 #[test]
236 fn test_merchant_error() {
237 let error = BridgeError::MerchantError("invalid response".into());
238 assert!(error.to_string().contains("Invalid merchant response"));
239 }
240
241 #[test]
242 fn test_invalid_consumer_id_error() {
243 let error = BridgeError::InvalidConsumerId("invalid@id".to_owned());
244 assert_eq!(error.to_string(), "Invalid consumer ID: invalid@id");
245 }
246
247 #[test]
248 fn test_invalid_merchant_url_error() {
249 let error = BridgeError::InvalidMerchantUrl("http://example.com".to_owned());
250 assert_eq!(error.to_string(), "Invalid merchant URL: http://example.com");
251 }
252}