Skip to content

Commit 97bfda1

Browse files
committed
fix(node): opaque AppError::Internal HTTP bodies
Log the real error server-side and return a generic message so unauthenticated surfaces like GET /ipfs/{cid} cannot leak sqlx/DB detail in 500 responses (#226).
1 parent 111cff7 commit 97bfda1

1 file changed

Lines changed: 39 additions & 5 deletions

File tree

‎crates/gitlawb-node/src/error.rs‎

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ impl From<anyhow::Error> for AppError {
8585
}
8686
}
8787

88+
/// Generic client-facing message for `AppError::Internal`. The real error is
89+
/// logged server-side; never put sqlx/anyhow detail in the HTTP body (#226).
90+
pub const INTERNAL_ERROR_MESSAGE: &str = "an internal error occurred";
91+
8892
impl IntoResponse for AppError {
8993
fn into_response(self) -> Response {
9094
// iCaptcha challenges carry structured discovery so clients don't have to
@@ -148,11 +152,16 @@ impl IntoResponse for AppError {
148152
DB_UNAVAILABLE_MESSAGE.into(),
149153
),
150154
AppError::Db(e) => (StatusCode::INTERNAL_SERVER_ERROR, "db_error", e.to_string()),
151-
AppError::Internal(e) => (
152-
StatusCode::INTERNAL_SERVER_ERROR,
153-
"internal_error",
154-
e.to_string(),
155-
),
155+
// Opaque body: raw sqlx/anyhow text must not reach unauthenticated
156+
// callers (GET /ipfs/{cid} and siblings map DB failures here) (#226).
157+
AppError::Internal(e) => {
158+
tracing::error!(error = %e, "internal error");
159+
(
160+
StatusCode::INTERNAL_SERVER_ERROR,
161+
"internal_error",
162+
INTERNAL_ERROR_MESSAGE.into(),
163+
)
164+
}
156165
};
157166

158167
let body = Json(json!({
@@ -169,6 +178,7 @@ pub type Result<T> = std::result::Result<T, AppError>;
169178
#[cfg(test)]
170179
mod tests {
171180
use super::*;
181+
use serde_json::Value;
172182

173183
#[test]
174184
fn timeout_maps_to_504_distinct_from_git_500() {
@@ -182,4 +192,28 @@ mod tests {
182192
StatusCode::INTERNAL_SERVER_ERROR
183193
);
184194
}
195+
196+
/// #226: raw sqlx/DB detail must never appear in the Internal 500 body.
197+
#[tokio::test]
198+
async fn internal_error_body_is_opaque() {
199+
let leak = "error returned from database: relation \"repos\" does not exist";
200+
let resp = AppError::Internal(anyhow::anyhow!("{leak}")).into_response();
201+
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
202+
203+
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
204+
.await
205+
.expect("read body");
206+
let v: Value = serde_json::from_slice(&bytes).expect("json body");
207+
assert_eq!(v["error"], "internal_error");
208+
assert_eq!(v["message"], INTERNAL_ERROR_MESSAGE);
209+
let rendered = String::from_utf8_lossy(&bytes);
210+
assert!(
211+
!rendered.contains("relation"),
212+
"DB schema detail leaked into body: {rendered}"
213+
);
214+
assert!(
215+
!rendered.contains(leak),
216+
"raw internal error leaked into body: {rendered}"
217+
);
218+
}
185219
}

0 commit comments

Comments
 (0)