fix(query): preserve structured backend errors
This commit is contained in:
1 parent
2e9dcdc26b
commit
bcafc4a474
58 files changed
+4182
-554
No files matched your search
@@ -1,11 +1,14 @@
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use dbx_core::backend_error::BackendError;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppError {
|
||||
pub message: String,
|
||||
pub status: StatusCode,
|
||||
pub error: Box<BackendError>,
|
||||
}
|
||||
|
||||
impl fmt::Display for AppError {
|
||||
@@ -16,32 +19,122 @@ impl fmt::Display for AppError {
|
||||
|
||||
impl AppError {
|
||||
pub fn internal(msg: impl Into<String>) -> Self {
|
||||
AppError { message: msg.into(), status: StatusCode::INTERNAL_SERVER_ERROR }
|
||||
Self::with_status(msg.into(), StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
pub fn bad_request(msg: impl Into<String>) -> Self {
|
||||
AppError { message: msg.into(), status: StatusCode::BAD_REQUEST }
|
||||
Self::with_status(msg.into(), StatusCode::BAD_REQUEST)
|
||||
}
|
||||
|
||||
pub fn not_found(msg: impl Into<String>) -> Self {
|
||||
AppError { message: msg.into(), status: StatusCode::NOT_FOUND }
|
||||
Self::with_status(msg.into(), StatusCode::NOT_FOUND)
|
||||
}
|
||||
|
||||
fn with_status(message: String, status: StatusCode) -> Self {
|
||||
let error = BackendError::from_legacy_string(&message);
|
||||
Self { message, status, error: Box::new(error) }
|
||||
}
|
||||
|
||||
pub fn from_backend_error(error: BackendError) -> Self {
|
||||
Self { message: error.code().to_string(), status: StatusCode::INTERNAL_SERVER_ERROR, error: Box::new(error) }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
(self.status, self.message).into_response()
|
||||
(self.status, Json(*self.error)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AppError {
|
||||
fn from(s: String) -> Self {
|
||||
AppError { message: s, status: StatusCode::INTERNAL_SERVER_ERROR }
|
||||
Self::internal(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for AppError {
|
||||
fn from(s: &str) -> Self {
|
||||
AppError { message: s.to_string(), status: StatusCode::INTERNAL_SERVER_ERROR }
|
||||
Self::internal(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BackendError> for AppError {
|
||||
fn from(error: BackendError) -> Self {
|
||||
Self::from_backend_error(error)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn legacy_errors_are_exposed_as_the_shared_backend_envelope() {
|
||||
let error = AppError::internal("database failed");
|
||||
assert_eq!(error.status, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(error.error.version(), 1);
|
||||
assert_eq!(error.error.code(), "DBX-LEGACY-0001");
|
||||
assert_eq!(error.message, "database failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_error_stays_small_for_route_result_types() {
|
||||
assert!(std::mem::size_of::<AppError>() <= 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_agent_text_keeps_catalog_identity_at_http_boundary() {
|
||||
let error = AppError::from(
|
||||
"Agent RPC error (-1): timed out\nDBX_AGENT_ERROR_DATA:{\"category\":\"timeout\",\"stage\":\"execute\"}",
|
||||
);
|
||||
assert_eq!(error.error.code(), "DBX-JDBC-9001");
|
||||
assert_eq!(error.error.source(), dbx_core::backend_error::BackendErrorSource::JdbcAgentLegacy);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_response_preserves_filtered_legacy_detail() {
|
||||
let response = AppError::internal("database failed").into_response();
|
||||
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(response.headers()[axum::http::header::CONTENT_TYPE], "application/json");
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(payload["version"], 1);
|
||||
assert_eq!(payload["code"], "DBX-LEGACY-0001");
|
||||
assert_eq!(payload["messageKey"], "backendErrors.legacy");
|
||||
assert_eq!(payload["detail"], "database failed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_response_preserves_filtered_structured_agent_detail() {
|
||||
use dbx_core::db::agent_driver::{
|
||||
AgentCallError, AgentErrorCategory, AgentErrorContext, AgentErrorStage, AgentOperationOutcome,
|
||||
AgentSessionDisposition,
|
||||
};
|
||||
|
||||
let error = BackendError::from_agent_call_error(&AgentCallError::Structured {
|
||||
rpc_code: -1,
|
||||
message: "relation customer_orders does not exist".to_string(),
|
||||
context: AgentErrorContext {
|
||||
contract_version: 1,
|
||||
category: AgentErrorCategory::Sql,
|
||||
retryable: false,
|
||||
session_disposition: AgentSessionDisposition::Keep,
|
||||
stage: AgentErrorStage::Execute,
|
||||
operation_outcome: AgentOperationOutcome::Unknown,
|
||||
agent_session_id: Some("session-1".to_string()),
|
||||
sql_state: Some("42P01".to_string()),
|
||||
vendor_code: None,
|
||||
exception_class: Some("java.sql.SQLException".to_string()),
|
||||
},
|
||||
});
|
||||
|
||||
let response = AppError::from(error).into_response();
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
|
||||
assert_eq!(payload["code"], "DBX-JDBC-4001");
|
||||
assert_eq!(payload["source"], "jdbcAgent");
|
||||
assert_eq!(payload["detail"], "relation customer_orders does not exist");
|
||||
assert!(payload["diagnostics"].get("agentSessionId").is_none());
|
||||
}
|
||||
}
|
||||
@@ -344,7 +344,7 @@ pub async fn execute_query(
|
||||
|
||||
tracing::debug!(connection_id = %req.connection_id, "execute_query");
|
||||
|
||||
let result = dbx_core::query::execute_sql_statement_with_options(
|
||||
let result = dbx_core::query::execute_sql_statement_with_options_typed(
|
||||
&state.app,
|
||||
&req.connection_id,
|
||||
&req.database,
|
||||
@@ -366,7 +366,7 @@ pub async fn execute_query(
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
.map_err(|error| AppError::from(error.into_backend_error()))?;
|
||||
|
||||
drop(registered);
|
||||
Ok(Json(result))
|
||||
@@ -390,7 +390,7 @@ pub async fn execute_multi(
|
||||
|
||||
tracing::debug!(connection_id = %req.connection_id, "execute_multi");
|
||||
|
||||
let result = dbx_core::query::execute_multi_core_with_options_for_client(
|
||||
let result = dbx_core::query::execute_multi_core_with_options_for_client_typed(
|
||||
&state.app,
|
||||
&req.connection_id,
|
||||
&req.database,
|
||||
@@ -412,10 +412,16 @@ pub async fn execute_multi(
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
.map_err(|error| AppError::from(error.into_backend_error()))?;
|
||||
|
||||
drop(registered);
|
||||
Ok(Json(result))
|
||||
Ok(execute_multi_response(result))
|
||||
}
|
||||
|
||||
fn execute_multi_response(
|
||||
result: Vec<dbx_core::query::ExecuteMultiResult>,
|
||||
) -> Json<Vec<dbx_core::query::ExecuteMultiResult>> {
|
||||
Json(result)
|
||||
}
|
||||
|
||||
pub async fn execute_batch(
|
||||
@@ -1004,4 +1010,35 @@ mod tests {
|
||||
// Missing connection cannot have applied statements.
|
||||
assert_eq!(log.executed_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_multi_response_preserves_nested_filtered_error_detail() {
|
||||
let result = dbx_core::query::ExecuteMultiResult {
|
||||
result: dbx_core::db::QueryResult {
|
||||
columns: vec!["Error".to_string()],
|
||||
column_types: vec![],
|
||||
column_sortables: vec![],
|
||||
spatial_columns: vec![],
|
||||
spatial_values: vec![],
|
||||
rows: vec![vec![serde_json::json!("relation customer_orders does not exist")]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
truncated: false,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
elasticsearch_raw_body: None,
|
||||
},
|
||||
execution_error: true,
|
||||
statement_index: Some(1),
|
||||
error: Some(dbx_core::backend_error::BackendError::from_sql_detail(
|
||||
"relation customer_orders does not exist",
|
||||
)),
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(execute_multi_response(vec![result]).0).unwrap();
|
||||
|
||||
assert_eq!(payload[0]["statement_index"], 1);
|
||||
assert_eq!(payload[0]["error"]["code"], "DBX-JDBC-4001");
|
||||
assert_eq!(payload[0]["error"]["detail"], "relation customer_orders does not exist");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user