use actix_web::http::StatusCode;
use serde_json::{Map, json};
use sqlx::error::DatabaseError;
use super::sql_sanitizer::{extract_metadata, sanitize_error_message};
use super::{ErrorCategory, ProcessedError, generate_trace_id};
pub fn process_sqlx_error(err: &sqlx::Error) -> ProcessedError {
process_sqlx_error_with_context(err, None)
}
pub fn process_sqlx_error_with_context(err: &sqlx::Error, context: Option<&str>) -> ProcessedError {
let trace_id = generate_trace_id();
match err {
sqlx::Error::Database(db_err) => {
process_database_error(db_err.as_ref(), &trace_id, context)
}
sqlx::Error::PoolTimedOut => create_connection_error(
"pool_timeout",
"Database connection pool timed out",
"The system is experiencing high load. Please try again in a moment.",
&trace_id,
),
sqlx::Error::PoolClosed => create_connection_error(
"pool_closed",
"Database connection pool is closed",
"The database connection pool has been shut down. Please contact support.",
&trace_id,
),
sqlx::Error::WorkerCrashed => create_internal_error(
"worker_crashed",
"Database worker thread crashed",
&trace_id,
),
sqlx::Error::Io(io_err) => create_connection_error(
"io_error",
"Database I/O error",
&format!("Network or I/O issue occurred: {}", io_err.kind()),
&trace_id,
),
sqlx::Error::Tls(tls_err) => create_connection_error(
"tls_error",
"Database TLS connection error",
&format!(
"Secure connection failed: {}",
sanitize_error_message(&tls_err.to_string())
),
&trace_id,
),
sqlx::Error::ColumnNotFound(column) => {
let mut error = ProcessedError::new(
ErrorCategory::QuerySyntax,
StatusCode::INTERNAL_SERVER_ERROR,
"column_not_found",
format!("Column '{}' not found in query results", column),
trace_id,
)
.with_hint(
"This may indicate a mismatch between the query and expected result structure.",
)
.with_metadata("column", json!(column));
if let Some(ctx) = context {
error = error.with_metadata("context", json!(ctx));
}
error
}
sqlx::Error::ColumnIndexOutOfBounds { index, len } => ProcessedError::new(
ErrorCategory::Internal,
StatusCode::INTERNAL_SERVER_ERROR,
"column_index_out_of_bounds",
format!(
"Column index {} is out of bounds (total columns: {})",
index, len
),
trace_id,
)
.with_metadata("index", json!(index))
.with_metadata("column_count", json!(len)),
sqlx::Error::TypeNotFound { type_name } => ProcessedError::new(
ErrorCategory::Internal,
StatusCode::INTERNAL_SERVER_ERROR,
"type_not_found",
format!("Database type '{}' not recognized", type_name),
trace_id,
)
.with_metadata("type_name", json!(type_name)),
sqlx::Error::ColumnDecode { index, source } => {
let sanitized_msg = sanitize_error_message(&source.to_string());
ProcessedError::new(
ErrorCategory::Internal,
StatusCode::INTERNAL_SERVER_ERROR,
"column_decode_error",
format!(
"Failed to decode column at index {}: {}",
index, sanitized_msg
),
trace_id,
)
.with_metadata("column_index", json!(index))
}
sqlx::Error::RowNotFound => ProcessedError::new(
ErrorCategory::NotFound,
StatusCode::NOT_FOUND,
"row_not_found",
"The requested record was not found",
trace_id,
)
.with_hint("Verify the identifier and try again."),
sqlx::Error::Migrate(_) => ProcessedError::new(
ErrorCategory::Internal,
StatusCode::INTERNAL_SERVER_ERROR,
"migration_error",
"Database migration error",
trace_id,
),
sqlx::Error::Configuration(config_err) => {
let sanitized_msg = sanitize_error_message(&config_err.to_string());
ProcessedError::new(
ErrorCategory::Internal,
StatusCode::INTERNAL_SERVER_ERROR,
"configuration_error",
format!("Database configuration error: {}", sanitized_msg),
trace_id,
)
}
_ => {
let sanitized_msg = sanitize_error_message(&err.to_string());
ProcessedError::new(
ErrorCategory::Internal,
StatusCode::INTERNAL_SERVER_ERROR,
"database_error",
format!("Database operation failed: {}", sanitized_msg),
trace_id,
)
}
}
}
fn process_database_error(
db_err: &dyn DatabaseError,
trace_id: &str,
context: Option<&str>,
) -> ProcessedError {
let code = db_err.code().map(|c| c.to_string());
let message = db_err.message();
let constraint = db_err.constraint();
let extracted = extract_metadata(message);
let mut metadata = Map::new();
if let Some(sql_code) = &code {
metadata.insert("sql_state".to_string(), json!(sql_code));
}
if let Some(constraint_name) = constraint.or(extracted.constraint_name.as_deref()) {
metadata.insert("constraint".to_string(), json!(constraint_name));
}
if let Some(column_name) = &extracted.column_name {
metadata.insert("column".to_string(), json!(column_name));
}
if let Some(table_name) = &extracted.table_name {
metadata.insert("table".to_string(), json!(table_name));
}
if let Some(ctx) = context {
metadata.insert("context".to_string(), json!(ctx));
}
match code.as_deref() {
Some("23505") => ProcessedError::new(
ErrorCategory::DatabaseConstraint,
StatusCode::CONFLICT,
"unique_violation",
"A record with this value already exists",
trace_id,
)
.with_hint("Please use a different value or update the existing record.")
.with_metadata_map(metadata),
Some("23503") => ProcessedError::new(
ErrorCategory::DatabaseConstraint,
StatusCode::UNPROCESSABLE_ENTITY,
"foreign_key_violation",
"The operation references a non-existent or inaccessible resource",
trace_id,
)
.with_hint("Ensure all referenced resources exist and are accessible.")
.with_metadata_map(metadata),
Some("23502") => ProcessedError::new(
ErrorCategory::Validation,
StatusCode::BAD_REQUEST,
"not_null_violation",
"A required field is missing",
trace_id,
)
.with_hint("Please provide all required fields.")
.with_metadata_map(metadata),
Some("23514") => ProcessedError::new(
ErrorCategory::DatabaseConstraint,
StatusCode::BAD_REQUEST,
"check_constraint_violation",
"The provided value does not meet the required constraints",
trace_id,
)
.with_hint("Please ensure the value meets all validation requirements.")
.with_metadata_map(metadata),
Some("42703") => ProcessedError::new(
ErrorCategory::QuerySyntax,
StatusCode::BAD_REQUEST,
"undefined_column",
"The specified column does not exist",
trace_id,
)
.with_hint("Please verify the column name and try again.")
.with_metadata_map(metadata),
Some("42P01") => ProcessedError::new(
ErrorCategory::QuerySyntax,
StatusCode::BAD_REQUEST,
"undefined_table",
"The specified table does not exist",
trace_id,
)
.with_hint("Please verify the table name and ensure it has been created.")
.with_metadata_map(metadata),
Some("42601") => ProcessedError::new(
ErrorCategory::QuerySyntax,
StatusCode::BAD_REQUEST,
"syntax_error",
"The query contains a syntax error",
trace_id,
)
.with_hint("Please check your query syntax.")
.with_metadata_map(metadata),
Some("42501") => ProcessedError::new(
ErrorCategory::Authorization,
StatusCode::FORBIDDEN,
"insufficient_privilege",
"You do not have permission to perform this operation",
trace_id,
)
.with_hint("Please contact an administrator for access.")
.with_metadata_map(metadata),
Some(code) if code.starts_with("08") => ProcessedError::new(
ErrorCategory::DatabaseConnection,
StatusCode::SERVICE_UNAVAILABLE,
"connection_error",
"Database connection issue",
trace_id,
)
.with_hint("The database is temporarily unavailable. Please try again.")
.with_metadata_map(metadata),
_ => {
let sanitized_msg = sanitize_error_message(message);
ProcessedError::new(
ErrorCategory::Internal,
StatusCode::INTERNAL_SERVER_ERROR,
"database_error",
format!("Database operation failed: {}", sanitized_msg),
trace_id,
)
.with_metadata_map(metadata)
}
}
}
fn create_connection_error(
code: &'static str,
message: &str,
hint: &str,
trace_id: &str,
) -> ProcessedError {
ProcessedError::new(
ErrorCategory::DatabaseConnection,
StatusCode::SERVICE_UNAVAILABLE,
code,
message,
trace_id,
)
.with_hint(hint)
}
fn create_internal_error(code: &'static str, message: &str, trace_id: &str) -> ProcessedError {
ProcessedError::new(
ErrorCategory::Internal,
StatusCode::INTERNAL_SERVER_ERROR,
code,
message,
trace_id,
)
.with_hint("An unexpected error occurred. Please contact support if this persists.")
}