Skip to content

Commit 47e4c02

Browse files
parse server error messages
This commit adds a parser to the Postgres error message, providing better error messages. Implemented based in: https://www.postgresql.org/docs/12/protocol-error-fields.html Signed-off-by: Sebastian Webber <sebastian@swebber.me>
1 parent 1cde74f commit 47e4c02

File tree

1 file changed

+290
-6
lines changed

1 file changed

+290
-6
lines changed

src/server.rs

Lines changed: 290 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use log::{debug, error, info, trace, warn};
66
use parking_lot::{Mutex, RwLock};
77
use postgres_protocol::message;
88
use std::collections::{BTreeSet, HashMap};
9+
use std::fmt::{Display, Formatter};
910
use std::io::Read;
1011
use std::net::IpAddr;
1112
use std::sync::Arc;
@@ -25,6 +26,7 @@ use crate::pool::ClientServerMap;
2526
use crate::scram::ScramSha256;
2627
use crate::stats::ServerStats;
2728
use std::io::Write;
29+
use std::str::FromStr;
2830

2931
use pin_project::pin_project;
3032

@@ -588,8 +590,7 @@ impl Server {
588590

589591
// An error message will be present.
590592
_ => {
591-
// Read the error message without the terminating null character.
592-
let mut error = vec![0u8; len as usize - 4 - 1];
593+
let mut error = vec![0u8; len as usize];
593594

594595
match stream.read_exact(&mut error).await {
595596
Ok(_) => (),
@@ -601,10 +602,9 @@ impl Server {
601602
}
602603
};
603604

604-
// TODO: the error message contains multiple fields; we can decode them and
605-
// present a prettier message to the user.
606-
// See: https://www.postgresql.org/docs/12/protocol-error-fields.html
607-
error!("Server error: {}", String::from_utf8_lossy(&error));
605+
let fields = PgErrorMsg::parse(error);
606+
trace!("error fields: {}", &fields);
607+
error!("server error: {}: {}", fields.severity, fields.message);
608608
}
609609
};
610610

@@ -1353,3 +1353,287 @@ impl Drop for Server {
13531353
);
13541354
}
13551355
}
1356+
1357+
// from https://www.postgresql.org/docs/12/protocol-error-fields.html
1358+
#[derive(Debug, Default, PartialEq)]
1359+
pub struct PgErrorMsg {
1360+
severity_localized: String, // S
1361+
severity: String, // V
1362+
code: String, // C
1363+
message: String, // M
1364+
detail: Option<String>, // D
1365+
hint: Option<String>, // H
1366+
position: Option<u32>, // P
1367+
internal_position: Option<u32>, // p
1368+
internal_query: Option<String>, // q
1369+
where_context: Option<String>, // W
1370+
schema_name: Option<String>, // s
1371+
table_name: Option<String>, // t
1372+
column_name: Option<String>, // c
1373+
data_type_name: Option<String>, // d
1374+
constraint_name: Option<String>, // n
1375+
file_name: Option<String>, // F
1376+
line: Option<u32>, // L
1377+
routine: Option<String>, // R
1378+
}
1379+
1380+
// TODO: implement with https://docs.rs/derive_more/latest/derive_more/
1381+
impl Display for PgErrorMsg {
1382+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1383+
write!(f, "[severity: {}]", self.severity)?;
1384+
write!(f, "[code: {}]", self.code)?;
1385+
write!(f, "[message: {}]", self.message)?;
1386+
if let Some(val) = &self.detail {
1387+
write!(f, "[detail: {val}]")?;
1388+
}
1389+
if let Some(val) = &self.hint {
1390+
write!(f, "[hint: {val}]")?;
1391+
}
1392+
if let Some(val) = &self.position {
1393+
write!(f, "[position: {val}]")?;
1394+
}
1395+
if let Some(val) = &self.internal_position {
1396+
write!(f, "[internal_position: {val}]")?;
1397+
}
1398+
if let Some(val) = &self.internal_query {
1399+
write!(f, "[internal_query: {val}]")?;
1400+
}
1401+
if let Some(val) = &self.internal_query {
1402+
write!(f, "[internal_query: {val}]")?;
1403+
}
1404+
if let Some(val) = &self.where_context {
1405+
write!(f, "[where: {val}]")?;
1406+
}
1407+
if let Some(val) = &self.schema_name {
1408+
write!(f, "[schema_name: {val}]")?;
1409+
}
1410+
if let Some(val) = &self.table_name {
1411+
write!(f, "[table_name: {val}]")?;
1412+
}
1413+
if let Some(val) = &self.column_name {
1414+
write!(f, "[column_name: {val}]")?;
1415+
}
1416+
if let Some(val) = &self.data_type_name {
1417+
write!(f, "[data_type_name: {val}]")?;
1418+
}
1419+
if let Some(val) = &self.constraint_name {
1420+
write!(f, "[constraint_name: {val}]")?;
1421+
}
1422+
if let Some(val) = &self.file_name {
1423+
write!(f, "[file_name: {val}]")?;
1424+
}
1425+
if let Some(val) = &self.line {
1426+
write!(f, "[line: {val}]")?;
1427+
}
1428+
if let Some(val) = &self.routine {
1429+
write!(f, "[routine: {val}]")?;
1430+
}
1431+
1432+
write!(f, " ")?;
1433+
1434+
Ok(())
1435+
}
1436+
}
1437+
1438+
impl PgErrorMsg {
1439+
pub fn parse(error_msg: Vec<u8>) -> PgErrorMsg {
1440+
let mut out = PgErrorMsg {
1441+
severity_localized: "".to_string(),
1442+
severity: "".to_string(),
1443+
code: "".to_string(),
1444+
message: "".to_string(),
1445+
detail: None,
1446+
hint: None,
1447+
position: None,
1448+
internal_position: None,
1449+
internal_query: None,
1450+
where_context: None,
1451+
schema_name: None,
1452+
table_name: None,
1453+
column_name: None,
1454+
data_type_name: None,
1455+
constraint_name: None,
1456+
file_name: None,
1457+
line: None,
1458+
routine: None,
1459+
};
1460+
for msg_part in error_msg.split(|v| *v == MESSAGE_TERMINATOR) {
1461+
if msg_part.is_empty() {
1462+
continue;
1463+
}
1464+
1465+
let msg_content = String::from_utf8_lossy(&msg_part[1..]).parse().unwrap();
1466+
1467+
match &msg_part[0] {
1468+
b'S' => {
1469+
out.severity_localized = msg_content;
1470+
}
1471+
b'V' => {
1472+
out.severity = msg_content;
1473+
}
1474+
b'C' => {
1475+
out.code = msg_content;
1476+
}
1477+
b'M' => {
1478+
out.message = msg_content;
1479+
}
1480+
b'D' => {
1481+
out.detail = Some(msg_content);
1482+
}
1483+
b'H' => {
1484+
out.hint = Some(msg_content);
1485+
}
1486+
b'P' => out.position = Some(u32::from_str(msg_content.as_str()).unwrap_or(0)),
1487+
b'p' => {
1488+
out.internal_position = Some(u32::from_str(msg_content.as_str()).unwrap_or(0))
1489+
}
1490+
b'q' => {
1491+
out.internal_query = Some(msg_content);
1492+
}
1493+
b'W' => {
1494+
out.where_context = Some(msg_content);
1495+
}
1496+
b's' => {
1497+
out.schema_name = Some(msg_content);
1498+
}
1499+
b't' => {
1500+
out.table_name = Some(msg_content);
1501+
}
1502+
b'c' => {
1503+
out.column_name = Some(msg_content);
1504+
}
1505+
b'd' => {
1506+
out.data_type_name = Some(msg_content);
1507+
}
1508+
b'n' => {
1509+
out.constraint_name = Some(msg_content);
1510+
}
1511+
b'F' => {
1512+
out.file_name = Some(msg_content);
1513+
}
1514+
b'L' => out.line = Some(u32::from_str(msg_content.as_str()).unwrap_or(0)),
1515+
b'R' => {
1516+
out.routine = Some(msg_content);
1517+
}
1518+
_ => {}
1519+
}
1520+
}
1521+
1522+
out
1523+
}
1524+
}
1525+
1526+
#[cfg(test)]
1527+
mod tests {
1528+
use crate::server::PgErrorMsg;
1529+
use log::{error, info};
1530+
1531+
fn field(kind: char, content: &str) -> Vec<u8> {
1532+
format!("{kind}{content}\0").as_bytes().to_vec()
1533+
}
1534+
1535+
#[test]
1536+
fn parse_fields() {
1537+
let mut complete_msg = vec![];
1538+
let severity = "FATAL";
1539+
complete_msg.extend(field('S', &severity));
1540+
complete_msg.extend(field('V', &severity));
1541+
1542+
let error_code = "29P02";
1543+
complete_msg.extend(field('C', &error_code));
1544+
let message = "password authentication failed for user \"wrong_user\"";
1545+
complete_msg.extend(field('M', &message));
1546+
let detail_msg = "super detailed message";
1547+
complete_msg.extend(field('D', &detail_msg));
1548+
let hint_msg = "hint detail here";
1549+
complete_msg.extend(field('H', &hint_msg));
1550+
complete_msg.extend(field('P', "123"));
1551+
complete_msg.extend(field('p', "234"));
1552+
let internal_query = "SELECT * from foo;";
1553+
complete_msg.extend(field('q', &internal_query));
1554+
let where_msg = "where goes here";
1555+
complete_msg.extend(field('W', &where_msg));
1556+
let schema_msg = "schema_name";
1557+
complete_msg.extend(field('s', &schema_msg));
1558+
let table_msg = "table_name";
1559+
complete_msg.extend(field('t', &table_msg));
1560+
let column_msg = "column_name";
1561+
complete_msg.extend(field('c', &column_msg));
1562+
let data_type_msg = "type_name";
1563+
complete_msg.extend(field('d', &data_type_msg));
1564+
let constraint_msg = "constraint_name";
1565+
complete_msg.extend(field('n', &constraint_msg));
1566+
let file_msg = "pgcat.c";
1567+
complete_msg.extend(field('F', &file_msg));
1568+
complete_msg.extend(field('L', "335"));
1569+
let routine_msg = "my_failing_routine";
1570+
complete_msg.extend(field('R', &routine_msg));
1571+
1572+
tracing_subscriber::fmt()
1573+
.with_max_level(tracing::Level::INFO)
1574+
.with_ansi(true)
1575+
.init();
1576+
1577+
info!("full message: {}", PgErrorMsg::parse(complete_msg.clone()));
1578+
assert_eq!(
1579+
PgErrorMsg {
1580+
severity_localized: severity.to_string(),
1581+
severity: severity.to_string(),
1582+
code: error_code.to_string(),
1583+
message: message.to_string(),
1584+
detail: Some(detail_msg.to_string()),
1585+
hint: Some(hint_msg.to_string()),
1586+
position: Some(123),
1587+
internal_position: Some(234),
1588+
internal_query: Some(internal_query.to_string()),
1589+
where_context: Some(where_msg.to_string()),
1590+
schema_name: Some(schema_msg.to_string()),
1591+
table_name: Some(table_msg.to_string()),
1592+
column_name: Some(column_msg.to_string()),
1593+
data_type_name: Some(data_type_msg.to_string()),
1594+
constraint_name: Some(constraint_msg.to_string()),
1595+
file_name: Some(file_msg.to_string()),
1596+
line: Some(335),
1597+
routine: Some(routine_msg.to_string()),
1598+
},
1599+
PgErrorMsg::parse(complete_msg)
1600+
);
1601+
1602+
let mut only_mandatory_msg = vec![];
1603+
only_mandatory_msg.extend(field('S', &severity));
1604+
only_mandatory_msg.extend(field('V', &severity));
1605+
only_mandatory_msg.extend(field('C', &error_code));
1606+
only_mandatory_msg.extend(field('M', &message));
1607+
only_mandatory_msg.extend(field('D', &detail_msg));
1608+
1609+
let err_fields = PgErrorMsg::parse(only_mandatory_msg.clone());
1610+
info!("only mandatory fields: {}", &err_fields);
1611+
error!(
1612+
"server error: {}: {}",
1613+
err_fields.severity, err_fields.message
1614+
);
1615+
assert_eq!(
1616+
PgErrorMsg {
1617+
severity_localized: severity.to_string(),
1618+
severity: severity.to_string(),
1619+
code: error_code.to_string(),
1620+
message: message.to_string(),
1621+
detail: Some(detail_msg.to_string()),
1622+
hint: None,
1623+
position: None,
1624+
internal_position: None,
1625+
internal_query: None,
1626+
where_context: None,
1627+
schema_name: None,
1628+
table_name: None,
1629+
column_name: None,
1630+
data_type_name: None,
1631+
constraint_name: None,
1632+
file_name: None,
1633+
line: None,
1634+
routine: None,
1635+
},
1636+
PgErrorMsg::parse(only_mandatory_msg)
1637+
);
1638+
}
1639+
}

0 commit comments

Comments
 (0)