Skip to main content

mssql_client/
error.rs

1//! Client error types.
2
3use std::sync::Arc;
4
5use thiserror::Error;
6
7/// Errors that can occur during client operations.
8#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum Error {
11    /// Connection failed.
12    #[error("connection failed: {0}")]
13    Connection(String),
14
15    /// Connection closed unexpectedly.
16    #[error("connection closed")]
17    ConnectionClosed,
18
19    /// Authentication failed.
20    #[error("authentication failed: {0}")]
21    Authentication(#[from] mssql_auth::AuthError),
22
23    /// TLS error.
24    #[cfg(feature = "tls")]
25    #[error("TLS error: {0}")]
26    Tls(#[from] mssql_tls::TlsError),
27
28    /// TLS error (when TLS feature is disabled, stores the message).
29    #[cfg(not(feature = "tls"))]
30    #[error("TLS error: {0}")]
31    Tls(String),
32
33    /// Protocol error from the TDS layer (preserves the source error chain).
34    #[error("protocol error: {0}")]
35    ProtocolError(#[from] tds_protocol::ProtocolError),
36
37    /// Protocol violation with a descriptive message.
38    #[error("protocol error: {0}")]
39    Protocol(String),
40
41    /// Codec error.
42    #[error("codec error: {0}")]
43    Codec(#[from] mssql_codec::CodecError),
44
45    /// Type conversion error.
46    #[error("type error: {0}")]
47    Type(#[from] mssql_types::TypeError),
48
49    /// Query execution error.
50    #[error("query error: {0}")]
51    Query(String),
52
53    /// Server returned an error.
54    #[error("server error {number} (severity {class}, state {state}): {message}{}", format_server_location(.server, .procedure, .line))]
55    Server {
56        /// Error number.
57        number: i32,
58        /// Error class/severity (0-25).
59        class: u8,
60        /// Error state.
61        state: u8,
62        /// Error message.
63        message: String,
64        /// Server name where error occurred.
65        server: Option<String>,
66        /// Stored procedure name (if applicable).
67        procedure: Option<String>,
68        /// Line number in the SQL batch or procedure.
69        line: u32,
70    },
71
72    /// Configuration error.
73    #[error("configuration error: {0}")]
74    Config(String),
75
76    /// TCP connection timeout occurred.
77    #[error("TCP connection timed out connecting to {host}:{port}")]
78    ConnectTimeout {
79        /// Target host.
80        host: String,
81        /// Target port.
82        port: u16,
83    },
84
85    /// TLS handshake timeout occurred.
86    #[error("TLS handshake timed out with {host}:{port}")]
87    TlsTimeout {
88        /// Target host.
89        host: String,
90        /// Target port.
91        port: u16,
92    },
93
94    /// Login/authentication response timeout occurred.
95    #[error("login timed out for {host}:{port}")]
96    LoginTimeout {
97        /// Target host.
98        host: String,
99        /// Target port.
100        port: u16,
101    },
102
103    /// Command execution timeout occurred.
104    #[error("command timed out")]
105    CommandTimeout,
106
107    /// Connection routing required (Azure SQL).
108    #[error("routing required to {host}:{port}")]
109    Routing {
110        /// Target host.
111        host: String,
112        /// Target port.
113        port: u16,
114    },
115
116    /// Too many redirects during connection.
117    #[error("too many redirects (max {max})")]
118    TooManyRedirects {
119        /// Maximum redirects allowed.
120        max: u8,
121    },
122
123    /// IO error (wrapped in Arc for Clone support).
124    #[error("IO error: {0}")]
125    Io(#[source] SharedIoError),
126
127    /// Invalid identifier (potential SQL injection attempt).
128    #[error("invalid identifier: {0}")]
129    InvalidIdentifier(String),
130
131    /// Connection pool exhausted.
132    #[error("connection pool exhausted")]
133    PoolExhausted,
134
135    /// Query cancellation error.
136    #[error("query cancellation failed: {0}")]
137    Cancel(String),
138
139    /// Query was cancelled by user request.
140    #[error("query cancelled")]
141    Cancelled,
142
143    /// SQL Browser service instance resolution failed.
144    #[error("SQL Browser resolution failed for instance '{instance}': {reason}")]
145    BrowserResolution {
146        /// The instance name that was being resolved.
147        instance: String,
148        /// Description of what went wrong.
149        reason: String,
150    },
151
152    /// FILESTREAM operation failed.
153    ///
154    /// This error occurs when opening or accessing a FILESTREAM BLOB fails,
155    /// typically due to a missing driver DLL, invalid path, or permission issue.
156    #[cfg(all(windows, feature = "filestream"))]
157    #[error("FILESTREAM error: {0}")]
158    FileStream(String),
159
160    /// Always Encrypted operation failed.
161    ///
162    /// This error occurs during CEK decryption, column value decryption, or
163    /// parameter encryption. Key material is never included in the error message.
164    #[cfg(feature = "always-encrypted")]
165    #[error("encryption error: {0}")]
166    Encryption(String),
167}
168
169// Note: From<mssql_tls::TlsError> and From<tds_protocol::ProtocolError> are
170// derived via #[from] on the enum variants above, preserving the full error chain.
171
172/// A cloneable wrapper around `std::io::Error` that preserves the error source chain.
173///
174/// `Arc<io::Error>` does not implement `std::error::Error`, which breaks
175/// `source()` chain traversal used by libraries like `anyhow` and `eyre`.
176/// This newtype bridges the gap.
177#[derive(Debug, Clone)]
178pub struct SharedIoError(Arc<std::io::Error>);
179
180impl std::fmt::Display for SharedIoError {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        self.0.fmt(f)
183    }
184}
185
186impl std::error::Error for SharedIoError {
187    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
188        self.0.source()
189    }
190}
191
192impl From<std::io::Error> for Error {
193    fn from(e: std::io::Error) -> Self {
194        Error::Io(SharedIoError(Arc::new(e)))
195    }
196}
197
198#[cfg(feature = "always-encrypted")]
199impl From<mssql_auth::EncryptionError> for Error {
200    fn from(e: mssql_auth::EncryptionError) -> Self {
201        // SECURITY: Do NOT include key material in the error message.
202        // EncryptionError::Display does not log keys, but we convert to
203        // String to ensure no internal state leaks.
204        Error::Encryption(e.to_string())
205    }
206}
207
208impl Error {
209    /// Check if this error is transient and may succeed on retry.
210    ///
211    /// Transient errors include timeouts, connection issues, and
212    /// certain server errors that may resolve themselves.
213    ///
214    /// Per ADR-009, the following server error codes are considered transient:
215    /// - 1205: Deadlock victim
216    /// - -2: Timeout
217    /// - 10928, 10929: Resource limit (Azure)
218    /// - 40197: Service error (Azure)
219    /// - 40501: Service busy (Azure)
220    /// - 40613: Database unavailable (Azure)
221    /// - 49918, 49919, 49920: Cannot process request (Azure)
222    /// - 4060: Cannot open database (may be transient during failover)
223    /// - 18456: Login failed (may be transient in Azure during failover)
224    #[must_use]
225    pub fn is_transient(&self) -> bool {
226        match self {
227            Self::ConnectTimeout { .. }
228            | Self::TlsTimeout { .. }
229            | Self::LoginTimeout { .. }
230            | Self::CommandTimeout
231            | Self::ConnectionClosed
232            | Self::Connection(_)
233            | Self::Routing { .. }
234            | Self::PoolExhausted
235            | Self::Io(_) => true,
236            Self::Server { number, .. } => Self::is_transient_server_error(*number),
237            _ => false,
238        }
239    }
240
241    /// Check if a server error number is transient (may succeed on retry).
242    ///
243    /// This follows the error codes specified in ADR-009.
244    ///
245    /// # Extending with custom error codes
246    ///
247    /// Applications with domain-specific transient error codes can compose
248    /// this method with their own logic:
249    ///
250    /// ```rust
251    /// use mssql_client::Error;
252    ///
253    /// fn is_transient_for_my_app(err: &Error) -> bool {
254    ///     // Check built-in transient codes first
255    ///     if err.is_transient() {
256    ///         return true;
257    ///     }
258    ///     // Add application-specific transient server errors
259    ///     if let Error::Server { number, .. } = err {
260    ///         matches!(number, 50001 | 50002) // custom app error codes
261    ///     } else {
262    ///         false
263    ///     }
264    /// }
265    /// ```
266    #[must_use]
267    pub fn is_transient_server_error(number: i32) -> bool {
268        matches!(
269            number,
270            1205 |      // Deadlock victim
271            -2 |        // Timeout
272            10928 |     // Resource limit (Azure)
273            10929 |     // Resource limit (Azure)
274            40197 |     // Service error (Azure)
275            40501 |     // Service busy (Azure)
276            40613 |     // Database unavailable (Azure)
277            49918 |     // Cannot process request (Azure)
278            49919 |     // Cannot process create/update (Azure)
279            49920 |     // Cannot process request (Azure)
280            4060 |      // Cannot open database
281            18456 // Login failed (may be transient in Azure)
282        )
283    }
284
285    /// Check if this is a terminal error that will never succeed on retry.
286    ///
287    /// Terminal errors include syntax errors, constraint violations, and
288    /// other errors that indicate programmer error or data issues.
289    ///
290    /// Per ADR-009, the following server error codes are terminal:
291    /// - 102: Syntax error
292    /// - 207: Invalid column
293    /// - 208: Invalid object
294    /// - 547: Constraint violation
295    /// - 2627: Unique constraint violation
296    /// - 2601: Duplicate key
297    #[must_use]
298    pub fn is_terminal(&self) -> bool {
299        match self {
300            Self::Config(_)
301            | Self::InvalidIdentifier(_)
302            | Self::Protocol(_)
303            | Self::ProtocolError(_)
304            | Self::Tls(_)
305            | Self::Authentication(_)
306            | Self::Cancel(_) => true,
307            Self::Server { number, .. } => Self::is_terminal_server_error(*number),
308            _ => false,
309        }
310    }
311
312    /// Check if a server error number is terminal (will never succeed on retry).
313    ///
314    /// This follows the error codes specified in ADR-009.
315    #[must_use]
316    pub fn is_terminal_server_error(number: i32) -> bool {
317        matches!(
318            number,
319            102 |       // Syntax error
320            207 |       // Invalid column
321            208 |       // Invalid object
322            547 |       // Constraint violation
323            2627 |      // Unique constraint violation
324            2601 // Duplicate key
325        )
326    }
327
328    /// Check if this error indicates a protocol/driver bug.
329    ///
330    /// Protocol errors typically indicate a bug in the driver implementation
331    /// rather than a user error or server issue. These are always terminal.
332    #[must_use]
333    pub fn is_protocol_error(&self) -> bool {
334        matches!(self, Self::Protocol(_) | Self::ProtocolError(_))
335    }
336
337    /// Check if this is a TLS/encryption error.
338    ///
339    /// TLS errors indicate certificate, handshake, or encryption failures.
340    /// These are terminal — TLS timeouts are reported as [`Error::TlsTimeout`] instead.
341    #[must_use]
342    pub fn is_tls_error(&self) -> bool {
343        matches!(self, Self::Tls(_) | Self::TlsTimeout { .. })
344    }
345
346    /// Check if this is an authentication error.
347    #[must_use]
348    pub fn is_authentication_error(&self) -> bool {
349        matches!(self, Self::Authentication(_))
350    }
351
352    /// Check if this is a configuration error.
353    ///
354    /// Configuration errors are always terminal — they indicate invalid
355    /// settings that cannot be resolved by retrying.
356    #[must_use]
357    pub fn is_config_error(&self) -> bool {
358        matches!(self, Self::Config(_))
359    }
360
361    /// Check if this is a server error with a specific number.
362    #[must_use]
363    pub fn is_server_error(&self, number: i32) -> bool {
364        matches!(self, Self::Server { number: n, .. } if *n == number)
365    }
366
367    /// Get the error class/severity if this is a server error.
368    ///
369    /// SQL Server error classes range from 0-25:
370    /// - 0-10: Informational
371    /// - 11-16: User errors
372    /// - 17-19: Resource/hardware errors
373    /// - 20-25: System errors (connection terminating)
374    #[must_use]
375    pub fn class(&self) -> Option<u8> {
376        match self {
377            Self::Server { class, .. } => Some(*class),
378            _ => None,
379        }
380    }
381
382    /// Alias for `class()` - returns error severity.
383    #[must_use]
384    pub fn severity(&self) -> Option<u8> {
385        self.class()
386    }
387}
388
389/// Format the server/procedure/line suffix for server error Display.
390fn format_server_location(
391    server: &Option<String>,
392    procedure: &Option<String>,
393    line: &u32,
394) -> String {
395    let mut parts = Vec::new();
396    if let Some(srv) = server {
397        if !srv.is_empty() {
398            parts.push(format!("server: {srv}"));
399        }
400    }
401    if let Some(proc) = procedure {
402        if !proc.is_empty() {
403            parts.push(format!("procedure: {proc}"));
404        }
405    }
406    if *line > 0 {
407        parts.push(format!("line: {line}"));
408    }
409    if parts.is_empty() {
410        String::new()
411    } else {
412        format!(" [{}]", parts.join(", "))
413    }
414}
415
416/// Result type for client operations.
417pub type Result<T> = std::result::Result<T, Error>;
418
419#[cfg(test)]
420#[allow(clippy::unwrap_used)]
421mod tests {
422    use super::*;
423    use std::sync::Arc;
424
425    fn make_server_error(number: i32) -> Error {
426        Error::Server {
427            number,
428            class: 16,
429            state: 1,
430            message: "Test error".to_string(),
431            server: None,
432            procedure: None,
433            line: 1,
434        }
435    }
436
437    #[test]
438    fn test_is_transient_connection_errors() {
439        assert!(
440            Error::ConnectTimeout {
441                host: "test".into(),
442                port: 1433
443            }
444            .is_transient()
445        );
446        assert!(
447            Error::TlsTimeout {
448                host: "test".into(),
449                port: 1433
450            }
451            .is_transient()
452        );
453        assert!(
454            Error::LoginTimeout {
455                host: "test".into(),
456                port: 1433
457            }
458            .is_transient()
459        );
460        assert!(Error::CommandTimeout.is_transient());
461        assert!(Error::ConnectionClosed.is_transient());
462        assert!(Error::PoolExhausted.is_transient());
463        assert!(
464            Error::Routing {
465                host: "test".into(),
466                port: 1433,
467            }
468            .is_transient()
469        );
470    }
471
472    #[test]
473    fn test_is_transient_io_error() {
474        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
475        assert!(Error::Io(SharedIoError(Arc::new(io_err))).is_transient());
476    }
477
478    #[test]
479    fn test_is_transient_server_errors_deadlock() {
480        // 1205 - Deadlock victim
481        assert!(make_server_error(1205).is_transient());
482    }
483
484    #[test]
485    fn test_is_transient_server_errors_timeout() {
486        // -2 - Timeout
487        assert!(make_server_error(-2).is_transient());
488    }
489
490    #[test]
491    fn test_is_transient_server_errors_azure() {
492        // Azure-specific transient errors
493        assert!(make_server_error(10928).is_transient()); // Resource limit
494        assert!(make_server_error(10929).is_transient()); // Resource limit
495        assert!(make_server_error(40197).is_transient()); // Service error
496        assert!(make_server_error(40501).is_transient()); // Service busy
497        assert!(make_server_error(40613).is_transient()); // Database unavailable
498        assert!(make_server_error(49918).is_transient()); // Cannot process request
499        assert!(make_server_error(49919).is_transient()); // Cannot process create/update
500        assert!(make_server_error(49920).is_transient()); // Cannot process request
501    }
502
503    #[test]
504    fn test_is_transient_server_errors_other() {
505        // Other transient errors
506        assert!(make_server_error(4060).is_transient()); // Cannot open database
507        assert!(make_server_error(18456).is_transient()); // Login failed (Azure failover)
508    }
509
510    #[test]
511    fn test_is_not_transient() {
512        // Non-transient errors
513        assert!(!Error::Config("bad config".into()).is_transient());
514        assert!(!Error::Query("syntax error".into()).is_transient());
515        assert!(!Error::InvalidIdentifier("bad id".into()).is_transient());
516        assert!(!make_server_error(102).is_transient()); // Syntax error
517    }
518
519    #[test]
520    fn test_is_terminal_server_errors() {
521        // Terminal SQL errors per ADR-009
522        assert!(make_server_error(102).is_terminal()); // Syntax error
523        assert!(make_server_error(207).is_terminal()); // Invalid column
524        assert!(make_server_error(208).is_terminal()); // Invalid object
525        assert!(make_server_error(547).is_terminal()); // Constraint violation
526        assert!(make_server_error(2627).is_terminal()); // Unique constraint violation
527        assert!(make_server_error(2601).is_terminal()); // Duplicate key
528    }
529
530    #[test]
531    fn test_is_terminal_config_errors() {
532        assert!(Error::Config("bad config".into()).is_terminal());
533        assert!(Error::InvalidIdentifier("bad id".into()).is_terminal());
534    }
535
536    #[test]
537    fn test_is_not_terminal() {
538        // Non-terminal errors (may be transient or other)
539        assert!(
540            !Error::ConnectTimeout {
541                host: "test".into(),
542                port: 1433
543            }
544            .is_terminal()
545        );
546        assert!(!make_server_error(1205).is_terminal()); // Deadlock - transient, not terminal
547        assert!(!make_server_error(40501).is_terminal()); // Service busy - transient
548    }
549
550    #[test]
551    fn test_transient_server_error_static() {
552        // Test the static helper function
553        assert!(Error::is_transient_server_error(1205));
554        assert!(Error::is_transient_server_error(40501));
555        assert!(!Error::is_transient_server_error(102));
556    }
557
558    #[test]
559    fn test_terminal_server_error_static() {
560        // Test the static helper function
561        assert!(Error::is_terminal_server_error(102));
562        assert!(Error::is_terminal_server_error(2627));
563        assert!(!Error::is_terminal_server_error(1205));
564    }
565
566    #[test]
567    fn test_error_class() {
568        let err = make_server_error(102);
569        assert_eq!(err.class(), Some(16));
570        assert_eq!(err.severity(), Some(16));
571
572        assert_eq!(
573            Error::ConnectTimeout {
574                host: "test".into(),
575                port: 1433
576            }
577            .class(),
578            None
579        );
580    }
581
582    #[test]
583    fn test_is_server_error() {
584        let err = make_server_error(102);
585        assert!(err.is_server_error(102));
586        assert!(!err.is_server_error(103));
587
588        assert!(
589            !Error::ConnectTimeout {
590                host: "test".into(),
591                port: 1433
592            }
593            .is_server_error(102)
594        );
595    }
596}