Skip to content

Commit 9ab1285

Browse files
parse server error messages (#543)
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 9ab1285

File tree

3 files changed

+308
-6
lines changed

3 files changed

+308
-6
lines changed

src/errors.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub enum Error {
1212
ProtocolSyncError(String),
1313
BadQuery(String),
1414
ServerError,
15+
ServerMessageParserError(String),
1516
ServerStartupError(String, ServerIdentifier),
1617
ServerAuthError(String, ServerIdentifier),
1718
BadConfig,

src/messages.rs

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ use crate::client::PREPARED_STATEMENT_COUNTER;
1111
use crate::config::get_config;
1212
use crate::errors::Error;
1313

14+
use crate::constants::MESSAGE_TERMINATOR;
1415
use std::collections::HashMap;
1516
use std::ffi::CString;
17+
use std::fmt::{Display, Formatter};
1618
use std::io::{BufRead, Cursor};
1719
use std::mem;
20+
use std::str::FromStr;
1821
use std::sync::atomic::Ordering;
1922
use std::time::Duration;
2023

@@ -1098,3 +1101,298 @@ pub fn prepared_statement_name() -> String {
10981101
PREPARED_STATEMENT_COUNTER.fetch_add(1, Ordering::SeqCst)
10991102
)
11001103
}
1104+
1105+
// from https://www.postgresql.org/docs/12/protocol-error-fields.html
1106+
#[derive(Debug, Default, PartialEq)]
1107+
pub struct PgErrorMsg {
1108+
pub severity_localized: String, // S
1109+
pub severity: String, // V
1110+
pub code: String, // C
1111+
pub message: String, // M
1112+
pub detail: Option<String>, // D
1113+
pub hint: Option<String>, // H
1114+
pub position: Option<u32>, // P
1115+
pub internal_position: Option<u32>, // p
1116+
pub internal_query: Option<String>, // q
1117+
pub where_context: Option<String>, // W
1118+
pub schema_name: Option<String>, // s
1119+
pub table_name: Option<String>, // t
1120+
pub column_name: Option<String>, // c
1121+
pub data_type_name: Option<String>, // d
1122+
pub constraint_name: Option<String>, // n
1123+
pub file_name: Option<String>, // F
1124+
pub line: Option<u32>, // L
1125+
pub routine: Option<String>, // R
1126+
}
1127+
1128+
// TODO: implement with https://docs.rs/derive_more/latest/derive_more/
1129+
impl Display for PgErrorMsg {
1130+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1131+
write!(f, "[severity: {}]", self.severity)?;
1132+
write!(f, "[code: {}]", self.code)?;
1133+
write!(f, "[message: {}]", self.message)?;
1134+
if let Some(val) = &self.detail {
1135+
write!(f, "[detail: {val}]")?;
1136+
}
1137+
if let Some(val) = &self.hint {
1138+
write!(f, "[hint: {val}]")?;
1139+
}
1140+
if let Some(val) = &self.position {
1141+
write!(f, "[position: {val}]")?;
1142+
}
1143+
if let Some(val) = &self.internal_position {
1144+
write!(f, "[internal_position: {val}]")?;
1145+
}
1146+
if let Some(val) = &self.internal_query {
1147+
write!(f, "[internal_query: {val}]")?;
1148+
}
1149+
if let Some(val) = &self.internal_query {
1150+
write!(f, "[internal_query: {val}]")?;
1151+
}
1152+
if let Some(val) = &self.where_context {
1153+
write!(f, "[where: {val}]")?;
1154+
}
1155+
if let Some(val) = &self.schema_name {
1156+
write!(f, "[schema_name: {val}]")?;
1157+
}
1158+
if let Some(val) = &self.table_name {
1159+
write!(f, "[table_name: {val}]")?;
1160+
}
1161+
if let Some(val) = &self.column_name {
1162+
write!(f, "[column_name: {val}]")?;
1163+
}
1164+
if let Some(val) = &self.data_type_name {
1165+
write!(f, "[data_type_name: {val}]")?;
1166+
}
1167+
if let Some(val) = &self.constraint_name {
1168+
write!(f, "[constraint_name: {val}]")?;
1169+
}
1170+
if let Some(val) = &self.file_name {
1171+
write!(f, "[file_name: {val}]")?;
1172+
}
1173+
if let Some(val) = &self.line {
1174+
write!(f, "[line: {val}]")?;
1175+
}
1176+
if let Some(val) = &self.routine {
1177+
write!(f, "[routine: {val}]")?;
1178+
}
1179+
1180+
write!(f, " ")?;
1181+
1182+
Ok(())
1183+
}
1184+
}
1185+
1186+
impl PgErrorMsg {
1187+
pub fn parse(error_msg: Vec<u8>) -> Result<PgErrorMsg, Error> {
1188+
let mut out = PgErrorMsg {
1189+
severity_localized: "".to_string(),
1190+
severity: "".to_string(),
1191+
code: "".to_string(),
1192+
message: "".to_string(),
1193+
detail: None,
1194+
hint: None,
1195+
position: None,
1196+
internal_position: None,
1197+
internal_query: None,
1198+
where_context: None,
1199+
schema_name: None,
1200+
table_name: None,
1201+
column_name: None,
1202+
data_type_name: None,
1203+
constraint_name: None,
1204+
file_name: None,
1205+
line: None,
1206+
routine: None,
1207+
};
1208+
for msg_part in error_msg.split(|v| *v == MESSAGE_TERMINATOR) {
1209+
if msg_part.is_empty() {
1210+
continue;
1211+
}
1212+
1213+
let msg_content = match String::from_utf8_lossy(&msg_part[1..]).parse() {
1214+
Ok(c) => c,
1215+
Err(err) => {
1216+
return Err(Error::ServerMessageParserError(format!(
1217+
"could not parse server message field. err {:?}",
1218+
err
1219+
)))
1220+
}
1221+
};
1222+
1223+
match &msg_part[0] {
1224+
b'S' => {
1225+
out.severity_localized = msg_content;
1226+
}
1227+
b'V' => {
1228+
out.severity = msg_content;
1229+
}
1230+
b'C' => {
1231+
out.code = msg_content;
1232+
}
1233+
b'M' => {
1234+
out.message = msg_content;
1235+
}
1236+
b'D' => {
1237+
out.detail = Some(msg_content);
1238+
}
1239+
b'H' => {
1240+
out.hint = Some(msg_content);
1241+
}
1242+
b'P' => out.position = Some(u32::from_str(msg_content.as_str()).unwrap_or(0)),
1243+
b'p' => {
1244+
out.internal_position = Some(u32::from_str(msg_content.as_str()).unwrap_or(0))
1245+
}
1246+
b'q' => {
1247+
out.internal_query = Some(msg_content);
1248+
}
1249+
b'W' => {
1250+
out.where_context = Some(msg_content);
1251+
}
1252+
b's' => {
1253+
out.schema_name = Some(msg_content);
1254+
}
1255+
b't' => {
1256+
out.table_name = Some(msg_content);
1257+
}
1258+
b'c' => {
1259+
out.column_name = Some(msg_content);
1260+
}
1261+
b'd' => {
1262+
out.data_type_name = Some(msg_content);
1263+
}
1264+
b'n' => {
1265+
out.constraint_name = Some(msg_content);
1266+
}
1267+
b'F' => {
1268+
out.file_name = Some(msg_content);
1269+
}
1270+
b'L' => out.line = Some(u32::from_str(msg_content.as_str()).unwrap_or(0)),
1271+
b'R' => {
1272+
out.routine = Some(msg_content);
1273+
}
1274+
_ => {}
1275+
}
1276+
}
1277+
1278+
Ok(out)
1279+
}
1280+
}
1281+
1282+
#[cfg(test)]
1283+
mod tests {
1284+
use crate::messages::PgErrorMsg;
1285+
use log::{error, info};
1286+
1287+
fn field(kind: char, content: &str) -> Vec<u8> {
1288+
format!("{kind}{content}\0").as_bytes().to_vec()
1289+
}
1290+
1291+
#[test]
1292+
fn parse_fields() {
1293+
let mut complete_msg = vec![];
1294+
let severity = "FATAL";
1295+
complete_msg.extend(field('S', &severity));
1296+
complete_msg.extend(field('V', &severity));
1297+
1298+
let error_code = "29P02";
1299+
complete_msg.extend(field('C', &error_code));
1300+
let message = "password authentication failed for user \"wrong_user\"";
1301+
complete_msg.extend(field('M', &message));
1302+
let detail_msg = "super detailed message";
1303+
complete_msg.extend(field('D', &detail_msg));
1304+
let hint_msg = "hint detail here";
1305+
complete_msg.extend(field('H', &hint_msg));
1306+
complete_msg.extend(field('P', "123"));
1307+
complete_msg.extend(field('p', "234"));
1308+
let internal_query = "SELECT * from foo;";
1309+
complete_msg.extend(field('q', &internal_query));
1310+
let where_msg = "where goes here";
1311+
complete_msg.extend(field('W', &where_msg));
1312+
let schema_msg = "schema_name";
1313+
complete_msg.extend(field('s', &schema_msg));
1314+
let table_msg = "table_name";
1315+
complete_msg.extend(field('t', &table_msg));
1316+
let column_msg = "column_name";
1317+
complete_msg.extend(field('c', &column_msg));
1318+
let data_type_msg = "type_name";
1319+
complete_msg.extend(field('d', &data_type_msg));
1320+
let constraint_msg = "constraint_name";
1321+
complete_msg.extend(field('n', &constraint_msg));
1322+
let file_msg = "pgcat.c";
1323+
complete_msg.extend(field('F', &file_msg));
1324+
complete_msg.extend(field('L', "335"));
1325+
let routine_msg = "my_failing_routine";
1326+
complete_msg.extend(field('R', &routine_msg));
1327+
1328+
tracing_subscriber::fmt()
1329+
.with_max_level(tracing::Level::INFO)
1330+
.with_ansi(true)
1331+
.init();
1332+
1333+
info!(
1334+
"full message: {}",
1335+
PgErrorMsg::parse(complete_msg.clone()).unwrap()
1336+
);
1337+
assert_eq!(
1338+
PgErrorMsg {
1339+
severity_localized: severity.to_string(),
1340+
severity: severity.to_string(),
1341+
code: error_code.to_string(),
1342+
message: message.to_string(),
1343+
detail: Some(detail_msg.to_string()),
1344+
hint: Some(hint_msg.to_string()),
1345+
position: Some(123),
1346+
internal_position: Some(234),
1347+
internal_query: Some(internal_query.to_string()),
1348+
where_context: Some(where_msg.to_string()),
1349+
schema_name: Some(schema_msg.to_string()),
1350+
table_name: Some(table_msg.to_string()),
1351+
column_name: Some(column_msg.to_string()),
1352+
data_type_name: Some(data_type_msg.to_string()),
1353+
constraint_name: Some(constraint_msg.to_string()),
1354+
file_name: Some(file_msg.to_string()),
1355+
line: Some(335),
1356+
routine: Some(routine_msg.to_string()),
1357+
},
1358+
PgErrorMsg::parse(complete_msg).unwrap()
1359+
);
1360+
1361+
let mut only_mandatory_msg = vec![];
1362+
only_mandatory_msg.extend(field('S', &severity));
1363+
only_mandatory_msg.extend(field('V', &severity));
1364+
only_mandatory_msg.extend(field('C', &error_code));
1365+
only_mandatory_msg.extend(field('M', &message));
1366+
only_mandatory_msg.extend(field('D', &detail_msg));
1367+
1368+
let err_fields = PgErrorMsg::parse(only_mandatory_msg.clone()).unwrap();
1369+
info!("only mandatory fields: {}", &err_fields);
1370+
error!(
1371+
"server error: {}: {}",
1372+
err_fields.severity, err_fields.message
1373+
);
1374+
assert_eq!(
1375+
PgErrorMsg {
1376+
severity_localized: severity.to_string(),
1377+
severity: severity.to_string(),
1378+
code: error_code.to_string(),
1379+
message: message.to_string(),
1380+
detail: Some(detail_msg.to_string()),
1381+
hint: None,
1382+
position: None,
1383+
internal_position: None,
1384+
internal_query: None,
1385+
where_context: None,
1386+
schema_name: None,
1387+
table_name: None,
1388+
column_name: None,
1389+
data_type_name: None,
1390+
constraint_name: None,
1391+
file_name: None,
1392+
line: None,
1393+
routine: None,
1394+
},
1395+
PgErrorMsg::parse(only_mandatory_msg).unwrap()
1396+
);
1397+
}
1398+
}

src/server.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -588,8 +588,7 @@ impl Server {
588588

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

594593
match stream.read_exact(&mut error).await {
595594
Ok(_) => (),
@@ -601,10 +600,14 @@ impl Server {
601600
}
602601
};
603602

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));
603+
let fields = match PgErrorMsg::parse(error) {
604+
Ok(f) => f,
605+
Err(err) => {
606+
return Err(err);
607+
}
608+
};
609+
trace!("error fields: {}", &fields);
610+
error!("server error: {}: {}", fields.severity, fields.message);
608611
}
609612
};
610613

0 commit comments

Comments
 (0)