Skip to main content

mssql_client/
error.rs

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