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}