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