@@ -168,8 +168,10 @@ impl IntoResponse for AppError {
168168 }
169169 // Opaque body: handlers that map with `.map_err(AppError::Internal)`
170170 // (e.g. GET /ipfs/{cid}) land here; other DB failures usually hit `Db`.
171+ // Log `{e:#}` so context-wrapped anyhow chains keep the leaf cause
172+ // (Display alone is only the outermost layer; see api/repos.rs).
171173 AppError :: Internal ( e) => {
172- tracing:: error!( error = %e , "internal error" ) ;
174+ tracing:: error!( error = %format! ( "{e:#}" ) , "internal error" ) ;
173175 (
174176 StatusCode :: INTERNAL_SERVER_ERROR ,
175177 "internal_error" ,
@@ -192,7 +194,7 @@ pub type Result<T> = std::result::Result<T, AppError>;
192194#[ cfg( test) ]
193195mod tests {
194196 use super :: * ;
195- use serde_json:: Value ;
197+ use serde_json:: { json , Value } ;
196198
197199 #[ test]
198200 fn timeout_maps_to_504_distinct_from_git_500 ( ) {
@@ -218,16 +220,14 @@ mod tests {
218220 . await
219221 . expect ( "read body" ) ;
220222 let v: Value = serde_json:: from_slice ( & bytes) . expect ( "json body" ) ;
221- assert_eq ! ( v[ "error" ] , "internal_error" ) ;
222- assert_eq ! ( v[ "message" ] , INTERNAL_ERROR_MESSAGE ) ;
223- let rendered = String :: from_utf8_lossy ( & bytes) ;
224- assert ! (
225- !rendered. contains( "relation" ) ,
226- "DB schema detail leaked into body: {rendered}"
227- ) ;
228- assert ! (
229- !rendered. contains( leak) ,
230- "raw internal error leaked into body: {rendered}"
223+ // Exact object: a new `detail` field with different sensitive text must
224+ // also fail, not only a repeat of the original error string.
225+ assert_eq ! (
226+ v,
227+ json!( {
228+ "error" : "internal_error" ,
229+ "message" : INTERNAL_ERROR_MESSAGE ,
230+ } )
231231 ) ;
232232 }
233233
@@ -244,16 +244,32 @@ mod tests {
244244 . await
245245 . expect ( "read body" ) ;
246246 let v: Value = serde_json:: from_slice ( & bytes) . expect ( "json body" ) ;
247- assert_eq ! ( v [ "error" ] , "db_error" ) ;
248- assert_eq ! ( v [ "message" ] , DB_ERROR_MESSAGE ) ;
249- let rendered = String :: from_utf8_lossy ( & bytes ) ;
250- assert ! (
251- !rendered . contains ( "is_public" ) ,
252- "DB schema detail leaked into body: {rendered}"
247+ assert_eq ! (
248+ v ,
249+ json! ( {
250+ "error" : "db_error" ,
251+ "message" : DB_ERROR_MESSAGE ,
252+ } )
253253 ) ;
254- assert ! (
255- !rendered. contains( "column" ) ,
256- "DB schema detail leaked into body: {rendered}"
254+ }
255+
256+ /// Connection-level failures must stay 503 `db_unavailable`, not collapse
257+ /// into the opaque 500 `db_error` arm if `db_unavailable` loses a variant.
258+ #[ tokio:: test]
259+ async fn db_pool_timeout_stays_503_unavailable ( ) {
260+ let resp = AppError :: Db ( sqlx:: Error :: PoolTimedOut ) . into_response ( ) ;
261+ assert_eq ! ( resp. status( ) , StatusCode :: SERVICE_UNAVAILABLE ) ;
262+
263+ let bytes = axum:: body:: to_bytes ( resp. into_body ( ) , usize:: MAX )
264+ . await
265+ . expect ( "read body" ) ;
266+ let v: Value = serde_json:: from_slice ( & bytes) . expect ( "json body" ) ;
267+ assert_eq ! (
268+ v,
269+ json!( {
270+ "error" : DB_UNAVAILABLE_CODE ,
271+ "message" : DB_UNAVAILABLE_MESSAGE ,
272+ } )
257273 ) ;
258274 }
259275}
0 commit comments