Skip to content

Commit 3a8cb39

Browse files
fix(server): sanitize API error responses to not leak internal details (#29)
Internal errors (DataFusion, Turso, Arrow, IO, Config) now return a generic "internal server error" message to clients instead of exposing implementation details like file paths and engine internals. User-facing errors (validation, schema, read/write path) remain descriptive. Full error details are still logged server-side via tracing::warn. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a5a2d53 commit 3a8cb39

1 file changed

Lines changed: 31 additions & 7 deletions

File tree

  • crates/dkdc-db-server/src

crates/dkdc-db-server/src/api.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,30 @@ fn classify_error(e: &dkdc_db_core::Error) -> StatusCode {
112112
}
113113
}
114114

115+
/// Return a user-safe error message, hiding internal details for server errors.
116+
///
117+
/// User-facing errors (Validation, Schema, WriteOnReadPath, ReadOnWritePath) are
118+
/// returned as-is since they contain information the caller needs to fix their request.
119+
/// Internal errors (Turso, DataFusion, Arrow, Io, Config) are replaced with a generic
120+
/// message — the full error is already logged server-side via tracing::warn.
121+
fn sanitize_error(e: &dkdc_db_core::Error) -> String {
122+
match e {
123+
dkdc_db_core::Error::Validation(msg) => format!("validation error: {msg}"),
124+
dkdc_db_core::Error::WriteOnReadPath(msg) => {
125+
format!("write attempted through read path: {msg}")
126+
}
127+
dkdc_db_core::Error::ReadOnWritePath(msg) => {
128+
format!("read attempted through write path: {msg}")
129+
}
130+
dkdc_db_core::Error::Schema(msg) => format!("schema error: {msg}"),
131+
dkdc_db_core::Error::Turso(_)
132+
| dkdc_db_core::Error::DataFusion(_)
133+
| dkdc_db_core::Error::Arrow(_)
134+
| dkdc_db_core::Error::Io(_)
135+
| dkdc_db_core::Error::Config(_) => "internal server error".to_string(),
136+
}
137+
}
138+
115139
pub fn batches_to_response(batches: &[dkdc_db_core::RecordBatch]) -> QueryResponse {
116140
let mut columns = Vec::new();
117141
let mut rows = Vec::new();
@@ -183,7 +207,7 @@ async fn create_db(
183207
}
184208
Err(e) => {
185209
tracing::warn!(db = %req.name, error = %e, "create_db failed");
186-
error_response(classify_error(&e), e).into_response()
210+
error_response(classify_error(&e), sanitize_error(&e)).into_response()
187211
}
188212
}
189213
}
@@ -196,7 +220,7 @@ async fn drop_db(State(mgr): State<AppState>, Path(name): Path<String>) -> impl
196220
}
197221
Err(e) => {
198222
tracing::warn!(db = %name, error = %e, "drop_db failed");
199-
error_response(classify_error(&e), e).into_response()
223+
error_response(classify_error(&e), sanitize_error(&e)).into_response()
200224
}
201225
}
202226
}
@@ -218,7 +242,7 @@ async fn execute(
218242
}
219243
Err(e) => {
220244
tracing::warn!(db = %name, error = %e, "execute failed");
221-
error_response(classify_error(&e), e).into_response()
245+
error_response(classify_error(&e), sanitize_error(&e)).into_response()
222246
}
223247
}
224248
}
@@ -228,7 +252,7 @@ async fn query(State(mgr): State<AppState>, Json(req): Json<SqlRequest>) -> impl
228252
Ok(batches) => (StatusCode::OK, Json(batches_to_response(&batches))).into_response(),
229253
Err(e) => {
230254
tracing::warn!(error = %e, "query failed");
231-
error_response(classify_error(&e), e).into_response()
255+
error_response(classify_error(&e), sanitize_error(&e)).into_response()
232256
}
233257
}
234258
}
@@ -242,7 +266,7 @@ async fn query_oltp(
242266
Ok(batches) => (StatusCode::OK, Json(batches_to_response(&batches))).into_response(),
243267
Err(e) => {
244268
tracing::warn!(db = %name, error = %e, "query_oltp failed");
245-
error_response(classify_error(&e), e).into_response()
269+
error_response(classify_error(&e), sanitize_error(&e)).into_response()
246270
}
247271
}
248272
}
@@ -252,7 +276,7 @@ async fn list_tables(State(mgr): State<AppState>, Path(name): Path<String>) -> i
252276
Ok(tables) => (StatusCode::OK, Json(tables)).into_response(),
253277
Err(e) => {
254278
tracing::warn!(db = %name, error = %e, "list_tables failed");
255-
error_response(classify_error(&e), e).into_response()
279+
error_response(classify_error(&e), sanitize_error(&e)).into_response()
256280
}
257281
}
258282
}
@@ -265,7 +289,7 @@ async fn table_schema(
265289
Ok(batches) => (StatusCode::OK, Json(batches_to_response(&batches))).into_response(),
266290
Err(e) => {
267291
tracing::warn!(db = %name, table = %table, error = %e, "table_schema failed");
268-
error_response(classify_error(&e), e).into_response()
292+
error_response(classify_error(&e), sanitize_error(&e)).into_response()
269293
}
270294
}
271295
}

0 commit comments

Comments
 (0)