Skip to main content

pgwire/
error.rs

1use std::fmt::Display;
2use std::io::Error as IOError;
3use thiserror::Error;
4
5use crate::messages::response::{ErrorResponse, NoticeResponse};
6
7/// Error type for pgwire server-side operations.
8#[derive(Error, Debug)]
9pub enum PgWireError {
10    #[error("Unsupported protocol version, received {0}.{1}")]
11    UnsupportedProtocolVersion(u16, u16),
12    #[error("Invalid CancelRequest message, code mismatch")]
13    InvalidCancelRequest,
14    #[error("Secret key length must be in [4, 256]")]
15    InvalidSecretKey,
16    #[error("Invalid message recevied, received {0}")]
17    InvalidMessageType(u8),
18    #[error("Invalid message length, expected max {0}, actual: {1}")]
19    MessageTooLarge(usize, usize),
20    #[error("Invalid target type, received {0}")]
21    InvalidTargetType(u8),
22    #[error("Invalid transaction status, received {0}")]
23    InvalidTransactionStatus(u8),
24    #[error("Invalid ssl request message")]
25    InvalidSSLRequestMessage,
26    #[error("Invalid gss encrypt request message")]
27    InvalidGssEncRequestMessage,
28    #[error("Invalid startup message")]
29    InvalidStartupMessage,
30    #[error("Invalid authentication message code: {0}")]
31    InvalidAuthenticationMessageCode(i32),
32    #[error("Invalid password message type, failed to coerce")]
33    FailedToCoercePasswordMessage,
34    #[error("Invalid SASL state")]
35    InvalidSASLState,
36    #[error("Unsupported SASL authentication method {0}")]
37    UnsupportedSASLAuthMethod(String),
38    #[error(transparent)]
39    IoError(#[from] std::io::Error),
40    #[error("Portal not found for name: {0}")]
41    PortalNotFound(String),
42    #[error("Cannot fetch portal in its Initial state, call start() first")]
43    PortalNotStarted,
44    #[error("Statement not found for name: {0}")]
45    StatementNotFound(String),
46    #[error("Parameter index out of bound: {0}")]
47    ParameterIndexOutOfBound(usize),
48    #[error("Cannot convert postgres type {0} to given rust type")]
49    InvalidRustTypeForParameter(String),
50    #[error("Failed to parse parameter: {0}")]
51    FailedToParseParameter(Box<dyn std::error::Error + Send + Sync>),
52    #[error("Failed to parse scram message: {0}")]
53    InvalidScramMessage(String),
54    #[error("Password authentication failed for user \"{0}\"")]
55    InvalidPassword(String),
56    #[error("Certificate algorithm is not supported")]
57    UnsupportedCertificateSignatureAlgorithm,
58    #[error("Username is required")]
59    UserNameRequired,
60    #[error("Connection is not ready for query")]
61    NotReadyForQuery,
62    #[error("canceling statement due to user request")]
63    QueryCanceled,
64    #[error("Invalid option value {0}")]
65    InvalidOptionValue(String),
66    #[error("{0}")]
67    InvalidOauthMessage(String),
68    #[error("{0}")]
69    OAuthAuthenticationFailed(String),
70    #[error("{0}")]
71    OAuthValidationError(String),
72    #[error("{0}")]
73    OauthAuthzIdError(String),
74    #[error(transparent)]
75    ApiError(#[from] Box<dyn std::error::Error + Send + Sync>),
76
77    #[error("User provided error: {0}")]
78    UserError(Box<ErrorInfo>),
79}
80
81impl From<PgWireError> for IOError {
82    fn from(e: PgWireError) -> Self {
83        IOError::other(e)
84    }
85}
86
87/// Result type for pgwire server-side operations.
88pub type PgWireResult<T> = Result<T, PgWireError>;
89
90/// Postgres error and notice message fields.
91///
92/// This part of protocol is defined in
93/// <https://www.postgresql.org/docs/18/protocol-error-fields.html>
94#[non_exhaustive]
95#[derive(new, Debug, Default)]
96pub struct ErrorInfo {
97    /// Severity can be one of `ERROR`, `FATAL`, or `PANIC` (in an error
98    /// message), or `WARNING`, `NOTICE`, `DEBUG`, `INFO`, or `LOG` (in a notice
99    /// message), or a localized translation of one of these.
100    pub severity: String,
101    /// Error code defined in
102    /// <https://www.postgresql.org/docs/current/errcodes-appendix.html>.
103    pub code: String,
104    /// Readable message.
105    pub message: String,
106    /// Optional secondary message.
107    #[new(default)]
108    pub detail: Option<String>,
109    /// Optional suggestion for fixing the issue.
110    #[new(default)]
111    pub hint: Option<String>,
112    /// Position: the field value is a decimal ASCII integer, indicating an error
113    /// cursor position as an index into the original query string.
114    #[new(default)]
115    pub position: Option<String>,
116    /// Internal position: this is defined the same as the P field, but it is
117    /// used when the cursor position refers to an internally generated command
118    /// rather than the one submitted by the client.
119    #[new(default)]
120    pub internal_position: Option<String>,
121    /// Internal query: the text of a failed internally-generated command.
122    #[new(default)]
123    pub internal_query: Option<String>,
124    /// Where: an indication of the context in which the error occurred.
125    #[new(default)]
126    pub where_context: Option<String>,
127    /// File: the file name of the source-code location where the error was reported.
128    #[new(default)]
129    pub file_name: Option<String>,
130    /// Line: the line number of the source-code location where the error was reported.
131    #[new(default)]
132    pub line: Option<usize>,
133    /// Routine: the name of the source-code routine reporting the error.
134    #[new(default)]
135    pub routine: Option<String>,
136    /// Severity: identical to the S field except that the contents are never localized.
137    /// This is present only in messages generated by PostgreSQL versions 9.6 and later.
138    #[new(default)]
139    pub severity_nonlocalized: Option<String>,
140    /// Schema name: if the error was associated with a specific database object,
141    /// the name of the schema containing that object, if any.
142    #[new(default)]
143    pub schema: Option<String>,
144    /// Table name: if the error was associated with a specific table, the name of the table.
145    #[new(default)]
146    pub table: Option<String>,
147    /// Column name: if the error was associated with a specific table column,
148    /// the name of the column.
149    #[new(default)]
150    pub column: Option<String>,
151    /// Data type name: if the error was associated with a specific data type,
152    /// the name of the data type.
153    #[new(default)]
154    pub datatype: Option<String>,
155    /// Constraint name: if the error was associated with a specific constraint,
156    /// the name of the constraint.
157    #[new(default)]
158    pub constraint: Option<String>,
159}
160
161impl Display for ErrorInfo {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        write!(
164            f,
165            "ErrorInfo: {}, {}, {}",
166            self.severity, self.code, self.message
167        )
168    }
169}
170
171impl ErrorInfo {
172    fn into_fields(self) -> Vec<(u8, String)> {
173        let mut fields = Vec::with_capacity(18);
174
175        fields.push((b'S', self.severity));
176        if let Some(value) = self.severity_nonlocalized {
177            fields.push((b'V', value));
178        }
179        fields.push((b'C', self.code));
180        fields.push((b'M', self.message));
181        if let Some(value) = self.detail {
182            fields.push((b'D', value));
183        }
184        if let Some(value) = self.hint {
185            fields.push((b'H', value));
186        }
187        if let Some(value) = self.position {
188            fields.push((b'P', value));
189        }
190        if let Some(value) = self.internal_position {
191            fields.push((b'p', value));
192        }
193        if let Some(value) = self.internal_query {
194            fields.push((b'q', value));
195        }
196        if let Some(value) = self.where_context {
197            fields.push((b'W', value));
198        }
199        if let Some(value) = self.file_name {
200            fields.push((b'F', value));
201        }
202        if let Some(value) = self.line {
203            fields.push((b'L', value.to_string()));
204        }
205        if let Some(value) = self.routine {
206            fields.push((b'R', value));
207        }
208        if let Some(value) = self.schema {
209            fields.push((b's', value));
210        }
211        if let Some(value) = self.table {
212            fields.push((b't', value));
213        }
214        if let Some(value) = self.column {
215            fields.push((b'c', value));
216        }
217        if let Some(value) = self.datatype {
218            fields.push((b'd', value));
219        }
220        if let Some(value) = self.constraint {
221            fields.push((b'n', value));
222        }
223
224        fields
225    }
226
227    /// Returns `true` if the error severity is `FATAL`.
228    pub fn is_fatal(&self) -> bool {
229        self.severity == "FATAL"
230    }
231}
232
233impl From<ErrorInfo> for ErrorResponse {
234    fn from(ei: ErrorInfo) -> ErrorResponse {
235        ErrorResponse::new(ei.into_fields())
236    }
237}
238
239impl From<ErrorResponse> for ErrorInfo {
240    fn from(value: ErrorResponse) -> Self {
241        let mut error_info = ErrorInfo::default();
242        for field in value.fields {
243            let (key, value) = field;
244            match key {
245                b'S' => {
246                    error_info.severity = value;
247                }
248                b'C' => {
249                    error_info.code = value;
250                }
251                b'M' => {
252                    error_info.message = value;
253                }
254                b'D' => {
255                    error_info.detail = Some(value);
256                }
257                b'H' => {
258                    error_info.hint = Some(value);
259                }
260                b'P' => {
261                    error_info.position = Some(value);
262                }
263                b'p' => {
264                    error_info.internal_position = Some(value);
265                }
266                b'q' => {
267                    error_info.internal_query = Some(value);
268                }
269                b'W' => {
270                    error_info.where_context = Some(value);
271                }
272                b'F' => {
273                    error_info.file_name = Some(value);
274                }
275                b'L' => {
276                    error_info.line = Some(value.parse().unwrap_or(0));
277                }
278                b'R' => {
279                    error_info.routine = Some(value);
280                }
281                b'V' => {
282                    error_info.severity_nonlocalized = Some(value);
283                }
284                b's' => {
285                    error_info.schema = Some(value);
286                }
287                b't' => {
288                    error_info.table = Some(value);
289                }
290                b'c' => {
291                    error_info.column = Some(value);
292                }
293                b'd' => {
294                    error_info.datatype = Some(value);
295                }
296                b'n' => {
297                    error_info.constraint = Some(value);
298                }
299                _ => {}
300            }
301        }
302        error_info
303    }
304}
305
306impl From<ErrorInfo> for NoticeResponse {
307    fn from(ei: ErrorInfo) -> NoticeResponse {
308        NoticeResponse::new(ei.into_fields())
309    }
310}
311
312impl From<PgWireError> for ErrorInfo {
313    fn from(error: PgWireError) -> Self {
314        match error {
315            PgWireError::UnsupportedProtocolVersion(_, _) => {
316                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
317            }
318            PgWireError::InvalidCancelRequest => {
319                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
320            }
321            PgWireError::InvalidMessageType(_) => {
322                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
323            }
324            PgWireError::InvalidTargetType(_) => {
325                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
326            }
327            PgWireError::MessageTooLarge(..) => {
328                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
329            }
330            PgWireError::InvalidTransactionStatus(_) => {
331                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
332            }
333            PgWireError::InvalidSSLRequestMessage => {
334                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
335            }
336            PgWireError::InvalidGssEncRequestMessage => {
337                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
338            }
339            PgWireError::InvalidStartupMessage => {
340                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
341            }
342            PgWireError::InvalidAuthenticationMessageCode(_) => {
343                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
344            }
345            PgWireError::FailedToCoercePasswordMessage => {
346                ErrorInfo::new("FATAL".to_owned(), "XX000".to_owned(), error.to_string())
347            }
348            PgWireError::InvalidSASLState => {
349                ErrorInfo::new("FATAL".to_owned(), "XX000".to_owned(), error.to_string())
350            }
351            PgWireError::UnsupportedSASLAuthMethod(_) => {
352                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
353            }
354            PgWireError::IoError(_) => {
355                ErrorInfo::new("FATAL".to_owned(), "58030".to_owned(), error.to_string())
356            }
357            PgWireError::PortalNotFound(_) => {
358                ErrorInfo::new("ERROR".to_owned(), "26000".to_owned(), error.to_string())
359            }
360            PgWireError::PortalNotStarted => {
361                ErrorInfo::new("ERROR".to_owned(), "XX000".to_owned(), error.to_string())
362            }
363            PgWireError::StatementNotFound(_) => {
364                ErrorInfo::new("ERROR".to_owned(), "26000".to_owned(), error.to_string())
365            }
366            PgWireError::ParameterIndexOutOfBound(_) => {
367                ErrorInfo::new("ERROR".to_owned(), "22023".to_owned(), error.to_string())
368            }
369            PgWireError::InvalidRustTypeForParameter(_) => {
370                ErrorInfo::new("ERROR".to_owned(), "22023".to_owned(), error.to_string())
371            }
372            PgWireError::FailedToParseParameter(_) => {
373                ErrorInfo::new("ERROR".to_owned(), "22P02".to_owned(), error.to_string())
374            }
375            PgWireError::InvalidScramMessage(_) => {
376                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
377            }
378            PgWireError::InvalidPassword(_) => {
379                ErrorInfo::new("FATAL".to_owned(), "28P01".to_owned(), error.to_string())
380            }
381            PgWireError::UnsupportedCertificateSignatureAlgorithm => {
382                ErrorInfo::new("FATAL".to_owned(), "0A000".to_owned(), error.to_string())
383            }
384            PgWireError::UserNameRequired => {
385                ErrorInfo::new("FATAL".to_owned(), "28000".to_owned(), error.to_string())
386            }
387            PgWireError::NotReadyForQuery => {
388                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
389            }
390            PgWireError::QueryCanceled => {
391                ErrorInfo::new("ERROR".to_owned(), "57014".to_owned(), error.to_string())
392            }
393            PgWireError::InvalidSecretKey => {
394                ErrorInfo::new("FATAL".to_owned(), "08P01".to_owned(), error.to_string())
395            }
396            PgWireError::ApiError(_) => {
397                ErrorInfo::new("ERROR".to_owned(), "XX000".to_owned(), error.to_string())
398            }
399            PgWireError::UserError(info) => *info,
400            PgWireError::InvalidOptionValue(_) => {
401                ErrorInfo::new("ERROR".to_owned(), "22023".to_owned(), error.to_string())
402            }
403            PgWireError::InvalidOauthMessage(_) => {
404                ErrorInfo::new("FATAL".to_string(), "08P01".to_string(), error.to_string())
405            }
406            PgWireError::OAuthAuthenticationFailed(_) => {
407                ErrorInfo::new("FATAL".to_string(), "08P01".to_string(), error.to_string())
408            }
409            PgWireError::OAuthValidationError(_) => {
410                ErrorInfo::new("FATAL".to_string(), "08P01".to_string(), error.to_string())
411            }
412            PgWireError::OauthAuthzIdError(_) => {
413                ErrorInfo::new("FATAL".to_string(), "0A000".to_string(), error.to_string())
414            }
415        }
416    }
417}
418
419#[cfg(feature = "client-api")]
420/// Error type for pgwire client-side operations.
421#[derive(Error, Debug)]
422pub enum PgWireClientError {
423    #[error("Failed to parse connection config, invalid value for: {0}")]
424    InvalidConfig(String),
425
426    #[error("Failed to parse connection config, unknown config: {0}")]
427    UnknownConfig(String),
428
429    #[error("Failed to parse utf8 value")]
430    InvalidUtf8ConfigValue(#[source] std::str::Utf8Error),
431
432    #[error("Unexpected EOF")]
433    UnexpectedEOF,
434
435    #[error("Unexpected remote message")]
436    UnexpectedMessage(Box<crate::messages::PgWireBackendMessage>),
437
438    #[error(transparent)]
439    IoError(#[from] std::io::Error),
440
441    #[error(transparent)]
442    PgWireError(#[from] PgWireError),
443
444    #[error("Error received from remote server: {0}")]
445    RemoteError(Box<ErrorInfo>),
446
447    #[error("Error parse command tag: {0}")]
448    InvalidTag(Box<dyn std::error::Error + Send + Sync>),
449
450    #[error("ALPN postgresql is required for direct connect.")]
451    AlpnRequired,
452
453    #[error("Failed to parse data: {0}")]
454    FromSqlError(Box<dyn std::error::Error + Send + Sync>),
455
456    #[error("Index out of bounds")]
457    DataRowIndexOutOfBounds,
458
459    #[error("Error from SCRAM authentication server: {0}")]
460    ScramError(String),
461
462    #[error("None of the server SASL auth methods is not supported by the client: {0:?}")]
463    UnsupportedSASLAuthMethods(Vec<String>),
464}
465
466#[cfg(feature = "client-api")]
467impl From<ErrorInfo> for PgWireClientError {
468    fn from(ei: ErrorInfo) -> PgWireClientError {
469        PgWireClientError::RemoteError(Box::new(ei))
470    }
471}
472
473#[cfg(feature = "client-api")]
474/// Result type for pgwire client-side operations.
475pub type PgWireClientResult<T> = Result<T, PgWireClientError>;
476
477#[cfg(test)]
478mod test {
479    use super::*;
480    use std::error::Error;
481
482    #[test]
483    fn test_error_notice_info() {
484        let error_info = ErrorInfo::new(
485            "FATAL".to_owned(),
486            "28P01".to_owned(),
487            "Password authentication failed".to_owned(),
488        );
489        assert_eq!("FATAL", error_info.severity);
490        assert_eq!("28P01", error_info.code);
491        assert_eq!("Password authentication failed", error_info.message);
492        assert!(error_info.file_name.is_none());
493    }
494
495    #[test]
496    fn test_error_send_and_sync() {
497        fn is_error_send_and_sync<T: Error + Send + Sync + 'static>() {}
498        is_error_send_and_sync::<PgWireError>();
499        #[cfg(feature = "client-api")]
500        is_error_send_and_sync::<PgWireClientError>();
501    }
502}