mirror of
https://github.com/neondatabase/neon.git
synced 2026-01-15 01:12:56 +00:00
## Problem
If a user provides a wrong database name in the connection string, it
should be logged as a user error, not postgres error.
I found 4 different places where we log such errors:
1. `proxy/src/stream.rs:193`, e.g.:
```
{"timestamp":"2025-07-15T11:33:35.660026Z","level":"INFO","message":"forwarding error to user","fields":{"kind":"postgres","msg":"database \"[redacted]\" does not exist"},"spans":{"connect_request#9":{"protocol":"tcp","session_id":"ce1f2c90-dfb5-44f7-b9e9-8b8535e8b9b8","conn_info":"[redacted]","ep":"[redacted]","role":"[redacted]"}},"thread_id":22,"task_id":"370407867","target":"proxy::stream","src":"proxy/src/stream.rs:193","extract":{"ep":"[redacted]","session_id":"ce1f2c90-dfb5-44f7-b9e9-8b8535e8b9b8"}}
```
2. `proxy/src/pglb/mod.rs:137`, e.g.:
```
{"timestamp":"2025-07-15T11:37:44.340497Z","level":"WARN","message":"per-client task finished with an error: Couldn't connect to compute node: db error: FATAL: database \"[redacted]\" does not exist","spans":{"connect_request#8":{"protocol":"tcp","session_id":"763baaac-d039-4f4d-9446-c149e32660eb","conn_info":"[redacted]","ep":"[redacted]","role":"[redacted]"}},"thread_id":14,"task_id":"866658139","target":"proxy::pglb","src":"proxy/src/pglb/mod.rs:137","extract":{"ep":"[redacted]","session_id":"763baaac-d039-4f4d-9446-c149e32660eb"}}
```
3. `proxy/src/serverless/mod.rs:451`, e.g. (note that the error is
repeated 4 times — retries?):
```
{"timestamp":"2025-07-15T11:37:54.515891Z","level":"WARN","message":"error in websocket connection: Couldn't connect to compute node: db error: FATAL: database \"[redacted]\" does not exist: Couldn't connect to compute node: db error: FATAL: database \"[redacted]\" does not exist: db error: FATAL: database \"[redacted]\" does not exist: FATAL: database \"[redacted]\" does not exist","spans":{"http_conn#8":{"conn_id":"ec7780db-a145-4f0e-90df-0ba35f41b828"},"connect_request#9":{"protocol":"ws","session_id":"1eaaeeec-b671-4153-b1f4-247839e4b1c7","conn_info":"[redacted]","ep":"[redacted]","role":"[redacted]"}},"thread_id":10,"task_id":"366331699","target":"proxy::serverless","src":"proxy/src/serverless/mod.rs:451","extract":{"conn_id":"ec7780db-a145-4f0e-90df-0ba35f41b828","ep":"[redacted]","session_id":"1eaaeeec-b671-4153-b1f4-247839e4b1c7"}}
```
4. `proxy/src/serverless/sql_over_http.rs:219`, e.g.
```
{"timestamp":"2025-07-15T10:32:34.866603Z","level":"INFO","message":"forwarding error to user","fields":{"kind":"postgres","error":"could not connect to postgres in compute","msg":"database \"[redacted]\" does not exist"},"spans":{"http_conn#19":{"conn_id":"7da08203-5dab-45e8-809f-503c9019ec6b"},"connect_request#5":{"protocol":"http","session_id":"68387f1c-cbc8-45b3-a7db-8bb1c55ca809","conn_info":"[redacted]","ep":"[redacted]","role":"[redacted]"}},"thread_id":17,"task_id":"16432250","target":"proxy::serverless::sql_over_http","src":"proxy/src/serverless/sql_over_http.rs:219","extract":{"conn_id":"7da08203-5dab-45e8-809f-503c9019ec6b","ep":"[redacted]","session_id":"68387f1c-cbc8-45b3-a7db-8bb1c55ca809"}}
```
This PR directly addresses 1 and 4. I _think_ it _should_ also help with
2 and 3, although in those places we don't seem to log `kind`, so I'm
not quite sure. I'm also confused why in 3 the error is repeated
multiple times.
## Summary of changes
Resolves https://github.com/neondatabase/neon/issues/9440
517 lines
16 KiB
Rust
517 lines
16 KiB
Rust
//! Errors.
|
|
|
|
use std::error::{self, Error as _Error};
|
|
use std::{fmt, io};
|
|
|
|
use fallible_iterator::FallibleIterator;
|
|
use postgres_protocol2::message::backend::{ErrorFields, ErrorResponseBody};
|
|
|
|
pub use self::sqlstate::*;
|
|
|
|
#[allow(clippy::unreadable_literal)]
|
|
pub mod sqlstate;
|
|
|
|
/// The severity of a Postgres error or notice.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
pub enum Severity {
|
|
/// PANIC
|
|
Panic,
|
|
/// FATAL
|
|
Fatal,
|
|
/// ERROR
|
|
Error,
|
|
/// WARNING
|
|
Warning,
|
|
/// NOTICE
|
|
Notice,
|
|
/// DEBUG
|
|
Debug,
|
|
/// INFO
|
|
Info,
|
|
/// LOG
|
|
Log,
|
|
}
|
|
|
|
impl fmt::Display for Severity {
|
|
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let s = match *self {
|
|
Severity::Panic => "PANIC",
|
|
Severity::Fatal => "FATAL",
|
|
Severity::Error => "ERROR",
|
|
Severity::Warning => "WARNING",
|
|
Severity::Notice => "NOTICE",
|
|
Severity::Debug => "DEBUG",
|
|
Severity::Info => "INFO",
|
|
Severity::Log => "LOG",
|
|
};
|
|
fmt.write_str(s)
|
|
}
|
|
}
|
|
|
|
impl Severity {
|
|
fn from_str(s: &str) -> Option<Severity> {
|
|
match s {
|
|
"PANIC" => Some(Severity::Panic),
|
|
"FATAL" => Some(Severity::Fatal),
|
|
"ERROR" => Some(Severity::Error),
|
|
"WARNING" => Some(Severity::Warning),
|
|
"NOTICE" => Some(Severity::Notice),
|
|
"DEBUG" => Some(Severity::Debug),
|
|
"INFO" => Some(Severity::Info),
|
|
"LOG" => Some(Severity::Log),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A Postgres error or notice.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct DbError {
|
|
severity: String,
|
|
parsed_severity: Option<Severity>,
|
|
code: SqlState,
|
|
message: String,
|
|
detail: Option<String>,
|
|
hint: Option<String>,
|
|
position: Option<ErrorPosition>,
|
|
where_: Option<String>,
|
|
schema: Option<String>,
|
|
table: Option<String>,
|
|
column: Option<String>,
|
|
datatype: Option<String>,
|
|
constraint: Option<String>,
|
|
file: Option<String>,
|
|
line: Option<u32>,
|
|
routine: Option<String>,
|
|
}
|
|
|
|
impl DbError {
|
|
pub fn new_test_error(code: SqlState, message: String) -> Self {
|
|
DbError {
|
|
severity: "ERROR".to_string(),
|
|
parsed_severity: Some(Severity::Error),
|
|
code,
|
|
message,
|
|
detail: None,
|
|
hint: None,
|
|
position: None,
|
|
where_: None,
|
|
schema: None,
|
|
table: None,
|
|
column: None,
|
|
datatype: None,
|
|
constraint: None,
|
|
file: None,
|
|
line: None,
|
|
routine: None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse(fields: &mut ErrorFields<'_>) -> io::Result<DbError> {
|
|
let mut severity = None;
|
|
let mut parsed_severity = None;
|
|
let mut code = None;
|
|
let mut message = None;
|
|
let mut detail = None;
|
|
let mut hint = None;
|
|
let mut normal_position = None;
|
|
let mut internal_position = None;
|
|
let mut internal_query = None;
|
|
let mut where_ = None;
|
|
let mut schema = None;
|
|
let mut table = None;
|
|
let mut column = None;
|
|
let mut datatype = None;
|
|
let mut constraint = None;
|
|
let mut file = None;
|
|
let mut line = None;
|
|
let mut routine = None;
|
|
|
|
while let Some(field) = fields.next()? {
|
|
match field.type_() {
|
|
b'S' => severity = Some(field.value().to_owned()),
|
|
b'C' => code = Some(SqlState::from_code(field.value())),
|
|
b'M' => message = Some(field.value().to_owned()),
|
|
b'D' => detail = Some(field.value().to_owned()),
|
|
b'H' => hint = Some(field.value().to_owned()),
|
|
b'P' => {
|
|
normal_position = Some(field.value().parse::<u32>().map_err(|_| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"`P` field did not contain an integer",
|
|
)
|
|
})?);
|
|
}
|
|
b'p' => {
|
|
internal_position = Some(field.value().parse::<u32>().map_err(|_| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"`p` field did not contain an integer",
|
|
)
|
|
})?);
|
|
}
|
|
b'q' => internal_query = Some(field.value().to_owned()),
|
|
b'W' => where_ = Some(field.value().to_owned()),
|
|
b's' => schema = Some(field.value().to_owned()),
|
|
b't' => table = Some(field.value().to_owned()),
|
|
b'c' => column = Some(field.value().to_owned()),
|
|
b'd' => datatype = Some(field.value().to_owned()),
|
|
b'n' => constraint = Some(field.value().to_owned()),
|
|
b'F' => file = Some(field.value().to_owned()),
|
|
b'L' => {
|
|
line = Some(field.value().parse::<u32>().map_err(|_| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"`L` field did not contain an integer",
|
|
)
|
|
})?);
|
|
}
|
|
b'R' => routine = Some(field.value().to_owned()),
|
|
b'V' => {
|
|
parsed_severity = Some(Severity::from_str(field.value()).ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"`V` field contained an invalid value",
|
|
)
|
|
})?);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Ok(DbError {
|
|
severity: severity
|
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "`S` field missing"))?,
|
|
parsed_severity,
|
|
code: code
|
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "`C` field missing"))?,
|
|
message: message
|
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "`M` field missing"))?,
|
|
detail,
|
|
hint,
|
|
position: match normal_position {
|
|
Some(position) => Some(ErrorPosition::Original(position)),
|
|
None => match internal_position {
|
|
Some(position) => Some(ErrorPosition::Internal {
|
|
position,
|
|
query: internal_query.ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"`q` field missing but `p` field present",
|
|
)
|
|
})?,
|
|
}),
|
|
None => None,
|
|
},
|
|
},
|
|
where_,
|
|
schema,
|
|
table,
|
|
column,
|
|
datatype,
|
|
constraint,
|
|
file,
|
|
line,
|
|
routine,
|
|
})
|
|
}
|
|
|
|
/// The field contents are ERROR, FATAL, or PANIC (in an error message),
|
|
/// or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message), or a
|
|
/// localized translation of one of these.
|
|
pub fn severity(&self) -> &str {
|
|
&self.severity
|
|
}
|
|
|
|
/// A parsed, nonlocalized version of `severity`. (PostgreSQL 9.6+)
|
|
pub fn parsed_severity(&self) -> Option<Severity> {
|
|
self.parsed_severity
|
|
}
|
|
|
|
/// The SQLSTATE code for the error.
|
|
pub fn code(&self) -> &SqlState {
|
|
&self.code
|
|
}
|
|
|
|
/// The primary human-readable error message.
|
|
///
|
|
/// This should be accurate but terse (typically one line).
|
|
pub fn message(&self) -> &str {
|
|
&self.message
|
|
}
|
|
|
|
/// An optional secondary error message carrying more detail about the
|
|
/// problem.
|
|
///
|
|
/// Might run to multiple lines.
|
|
pub fn detail(&self) -> Option<&str> {
|
|
self.detail.as_deref()
|
|
}
|
|
|
|
/// An optional suggestion what to do about the problem.
|
|
///
|
|
/// This is intended to differ from `detail` in that it offers advice
|
|
/// (potentially inappropriate) rather than hard facts. Might run to
|
|
/// multiple lines.
|
|
pub fn hint(&self) -> Option<&str> {
|
|
self.hint.as_deref()
|
|
}
|
|
|
|
/// An optional error cursor position into either the original query string
|
|
/// or an internally generated query.
|
|
pub fn position(&self) -> Option<&ErrorPosition> {
|
|
self.position.as_ref()
|
|
}
|
|
|
|
/// An indication of the context in which the error occurred.
|
|
///
|
|
/// Presently this includes a call stack traceback of active procedural
|
|
/// language functions and internally-generated queries. The trace is one
|
|
/// entry per line, most recent first.
|
|
pub fn where_(&self) -> Option<&str> {
|
|
self.where_.as_deref()
|
|
}
|
|
|
|
/// If the error was associated with a specific database object, the name
|
|
/// of the schema containing that object, if any. (PostgreSQL 9.3+)
|
|
pub fn schema(&self) -> Option<&str> {
|
|
self.schema.as_deref()
|
|
}
|
|
|
|
/// If the error was associated with a specific table, the name of the
|
|
/// table. (Refer to the schema name field for the name of the table's
|
|
/// schema.) (PostgreSQL 9.3+)
|
|
pub fn table(&self) -> Option<&str> {
|
|
self.table.as_deref()
|
|
}
|
|
|
|
/// If the error was associated with a specific table column, the name of
|
|
/// the column.
|
|
///
|
|
/// (Refer to the schema and table name fields to identify the table.)
|
|
/// (PostgreSQL 9.3+)
|
|
pub fn column(&self) -> Option<&str> {
|
|
self.column.as_deref()
|
|
}
|
|
|
|
/// If the error was associated with a specific data type, the name of the
|
|
/// data type. (Refer to the schema name field for the name of the data
|
|
/// type's schema.) (PostgreSQL 9.3+)
|
|
pub fn datatype(&self) -> Option<&str> {
|
|
self.datatype.as_deref()
|
|
}
|
|
|
|
/// If the error was associated with a specific constraint, the name of the
|
|
/// constraint.
|
|
///
|
|
/// Refer to fields listed above for the associated table or domain.
|
|
/// (For this purpose, indexes are treated as constraints, even if they
|
|
/// weren't created with constraint syntax.) (PostgreSQL 9.3+)
|
|
pub fn constraint(&self) -> Option<&str> {
|
|
self.constraint.as_deref()
|
|
}
|
|
|
|
/// The file name of the source-code location where the error was reported.
|
|
pub fn file(&self) -> Option<&str> {
|
|
self.file.as_deref()
|
|
}
|
|
|
|
/// The line number of the source-code location where the error was
|
|
/// reported.
|
|
pub fn line(&self) -> Option<u32> {
|
|
self.line
|
|
}
|
|
|
|
/// The name of the source-code routine reporting the error.
|
|
pub fn routine(&self) -> Option<&str> {
|
|
self.routine.as_deref()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for DbError {
|
|
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(fmt, "{}: {}", self.severity, self.message)?;
|
|
if let Some(detail) = &self.detail {
|
|
write!(fmt, "\nDETAIL: {detail}")?;
|
|
}
|
|
if let Some(hint) = &self.hint {
|
|
write!(fmt, "\nHINT: {hint}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl error::Error for DbError {}
|
|
|
|
/// Represents the position of an error in a query.
|
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
pub enum ErrorPosition {
|
|
/// A position in the original query.
|
|
Original(u32),
|
|
/// A position in an internally generated query.
|
|
Internal {
|
|
/// The byte position.
|
|
position: u32,
|
|
/// A query generated by the Postgres server.
|
|
query: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
enum Kind {
|
|
Io,
|
|
UnexpectedMessage,
|
|
Tls,
|
|
ToSql(usize),
|
|
FromSql(usize),
|
|
Column(String),
|
|
Closed,
|
|
Db,
|
|
Parse,
|
|
Encode,
|
|
Authentication,
|
|
Config,
|
|
Connect,
|
|
Timeout,
|
|
}
|
|
|
|
struct ErrorInner {
|
|
kind: Kind,
|
|
cause: Option<Box<dyn error::Error + Sync + Send>>,
|
|
}
|
|
|
|
/// An error communicating with the Postgres server.
|
|
pub struct Error(Box<ErrorInner>);
|
|
|
|
impl fmt::Debug for Error {
|
|
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
fmt.debug_struct("Error")
|
|
.field("kind", &self.0.kind)
|
|
.field("cause", &self.0.cause)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Error {
|
|
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match &self.0.kind {
|
|
Kind::Io => fmt.write_str("error communicating with the server")?,
|
|
Kind::UnexpectedMessage => fmt.write_str("unexpected message from server")?,
|
|
Kind::Tls => fmt.write_str("error performing TLS handshake")?,
|
|
Kind::ToSql(idx) => write!(fmt, "error serializing parameter {idx}")?,
|
|
Kind::FromSql(idx) => write!(fmt, "error deserializing column {idx}")?,
|
|
Kind::Column(column) => write!(fmt, "invalid column `{column}`")?,
|
|
Kind::Closed => fmt.write_str("connection closed")?,
|
|
Kind::Db => fmt.write_str("db error")?,
|
|
Kind::Parse => fmt.write_str("error parsing response from server")?,
|
|
Kind::Encode => fmt.write_str("error encoding message to server")?,
|
|
Kind::Authentication => fmt.write_str("authentication error")?,
|
|
Kind::Config => fmt.write_str("invalid configuration")?,
|
|
Kind::Connect => fmt.write_str("error connecting to server")?,
|
|
Kind::Timeout => fmt.write_str("timeout waiting for server")?,
|
|
};
|
|
if let Some(ref cause) = self.0.cause {
|
|
write!(fmt, ": {cause}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl error::Error for Error {
|
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
|
self.0.cause.as_ref().map(|e| &**e as _)
|
|
}
|
|
}
|
|
|
|
impl Error {
|
|
/// Consumes the error, returning its cause.
|
|
pub fn into_source(self) -> Option<Box<dyn error::Error + Sync + Send>> {
|
|
self.0.cause
|
|
}
|
|
|
|
/// Returns the source of this error if it was a `DbError`.
|
|
///
|
|
/// This is a simple convenience method.
|
|
pub fn as_db_error(&self) -> Option<&DbError> {
|
|
self.source().and_then(|e| e.downcast_ref::<DbError>())
|
|
}
|
|
|
|
/// Determines if the error was associated with closed connection.
|
|
pub fn is_closed(&self) -> bool {
|
|
self.0.kind == Kind::Closed
|
|
}
|
|
|
|
/// Returns the SQLSTATE error code associated with the error.
|
|
///
|
|
/// This is a convenience method that downcasts the cause to a `DbError` and returns its code.
|
|
pub fn code(&self) -> Option<&SqlState> {
|
|
self.as_db_error().map(DbError::code)
|
|
}
|
|
|
|
fn new(kind: Kind, cause: Option<Box<dyn error::Error + Sync + Send>>) -> Error {
|
|
Error(Box::new(ErrorInner { kind, cause }))
|
|
}
|
|
|
|
pub fn closed() -> Error {
|
|
Error::new(Kind::Closed, None)
|
|
}
|
|
|
|
pub fn unexpected_message() -> Error {
|
|
Error::new(Kind::UnexpectedMessage, None)
|
|
}
|
|
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
pub fn db(error: ErrorResponseBody) -> Error {
|
|
match DbError::parse(&mut error.fields()) {
|
|
Ok(e) => Error::new(Kind::Db, Some(Box::new(e))),
|
|
Err(e) => Error::new(Kind::Parse, Some(Box::new(e))),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse(e: io::Error) -> Error {
|
|
Error::new(Kind::Parse, Some(Box::new(e)))
|
|
}
|
|
|
|
pub(crate) fn encode(e: io::Error) -> Error {
|
|
Error::new(Kind::Encode, Some(Box::new(e)))
|
|
}
|
|
|
|
#[allow(clippy::wrong_self_convention)]
|
|
pub(crate) fn to_sql(e: Box<dyn error::Error + Sync + Send>, idx: usize) -> Error {
|
|
Error::new(Kind::ToSql(idx), Some(e))
|
|
}
|
|
|
|
pub(crate) fn from_sql(e: Box<dyn error::Error + Sync + Send>, idx: usize) -> Error {
|
|
Error::new(Kind::FromSql(idx), Some(e))
|
|
}
|
|
|
|
pub(crate) fn column(column: String) -> Error {
|
|
Error::new(Kind::Column(column), None)
|
|
}
|
|
|
|
pub(crate) fn tls(e: Box<dyn error::Error + Sync + Send>) -> Error {
|
|
Error::new(Kind::Tls, Some(e))
|
|
}
|
|
|
|
pub fn io(e: io::Error) -> Error {
|
|
Error::new(Kind::Io, Some(Box::new(e)))
|
|
}
|
|
|
|
pub(crate) fn authentication(e: Box<dyn error::Error + Sync + Send>) -> Error {
|
|
Error::new(Kind::Authentication, Some(e))
|
|
}
|
|
|
|
pub(crate) fn config(e: Box<dyn error::Error + Sync + Send>) -> Error {
|
|
Error::new(Kind::Config, Some(e))
|
|
}
|
|
|
|
pub(crate) fn connect(e: io::Error) -> Error {
|
|
Error::new(Kind::Connect, Some(Box::new(e)))
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
pub fn __private_api_timeout() -> Error {
|
|
Error::new(Kind::Timeout, None)
|
|
}
|
|
}
|