From b2da314fdd1c40c5da150ed73c8d365bf95e734b Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Tue, 11 Aug 2026 14:54:57 -0400 Subject: [PATCH] fix(api): log database failures in handlers that fall back to a bare 500 session, set_default_camo, list_opponents, list_suggested_opponents, list_matches, and match_status all turned a database error into a bare 500 with nothing in the log. This crate logs AWS and callback failures well; database failures on the hot path were the gap. session is hit on every page load, so a transient sqlite problem was a wave of unexplained 500s with no trace of why. Also covers the proxy's authorize(): db.get_match now logs before its 500, and db.is_match_player's fallback to false (kept - failing closed is correct) now logs a warn first, so a real seat-holder hitting a transient DB error is distinguishable in the log from a stranger at the door. Logging only: no control flow, status codes, or return values changed. Field style and message shape follow settle_own_camo's existing "camo: default could not be read" precedent. --- services/api/src/proxy.rs | 17 ++++++++++++-- services/api/src/routes.rs | 47 +++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/services/api/src/proxy.rs b/services/api/src/proxy.rs index 5f70192..03fe269 100644 --- a/services/api/src/proxy.rs +++ b/services/api/src/proxy.rs @@ -115,7 +115,10 @@ async fn authorize( let row = match state.db.get_match(id).await { Ok(Some(row)) => row, Ok(None) => return Err(StatusCode::NOT_FOUND.into_response()), - Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR.into_response()), + Err(_) => { + tracing::error!(match_id = id, "match proxy: match lookup failed"); + return Err(StatusCode::INTERNAL_SERVER_ERROR.into_response()); + } }; // Owner first: matches from before the seats table have only their owner // row. @@ -124,7 +127,17 @@ async fn authorize( .db .is_match_player(&row.id, &did) .await - .unwrap_or(false) + .unwrap_or_else(|_| { + // Fail closed - keep the caller out - but say so: otherwise a + // real seat-holder hitting a transient database error looks + // exactly like a stranger at this match's door. + tracing::warn!( + match_id = row.id, + did, + "match proxy: seat lookup failed, refusing" + ); + false + }) { // Not their match. The social identity check the URL promises. return Err(StatusCode::FORBIDDEN.into_response()); diff --git a/services/api/src/routes.rs b/services/api/src/routes.rs index 84c7eab..1dc0ca2 100644 --- a/services/api/src/routes.rs +++ b/services/api/src/routes.rs @@ -213,7 +213,10 @@ async fn session(State(state): State, jar: CookieJar) -> Response { let jar = CookieJar::new().add(state.signer.clear()); return (StatusCode::UNAUTHORIZED, jar).into_response(); } - Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + Err(_) => { + tracing::error!(did, "session: oauth session lookup failed"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } } match state.db.account_handle(&did).await { @@ -227,6 +230,7 @@ async fn session(State(state): State, jar: CookieJar) -> Response { // error, and this comes from the same row. Reporting "no // default" instead would have the editor draw the toggle // unset, which is a lie the player can act on. + tracing::error!(did, "session: default camo lookup failed"); return StatusCode::INTERNAL_SERVER_ERROR.into_response(); }; Json(serde_json::json!({ @@ -236,7 +240,10 @@ async fn session(State(state): State, jar: CookieJar) -> Response { })) .into_response() } - Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + Err(_) => { + tracing::error!(did, "session: handle lookup failed"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } } } @@ -571,10 +578,13 @@ async fn set_default_camo( tracing::info!(did, set = rkey.is_some(), "camo: default changed"); Json(serde_json::json!({ "defaultCamo": rkey })).into_response() } - Err(_) => message( - StatusCode::INTERNAL_SERVER_ERROR, - "That could not be saved just now.", - ), + Err(_) => { + tracing::error!(did, "camo: default could not be saved"); + message( + StatusCode::INTERNAL_SERVER_ERROR, + "That could not be saved just now.", + ) + } } } @@ -703,7 +713,10 @@ async fn list_opponents(State(state): State, jar: CookieJar) -> Respon }; match state.db.recent_opponents(&did, 12).await { Ok(opponents) => Json(serde_json::json!({ "opponents": opponents })).into_response(), - Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + Err(_) => { + tracing::error!(did, "opponents: recent-opponents lookup failed"); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } } } @@ -739,11 +752,17 @@ async fn list_suggested_opponents(State(state): State, jar: CookieJar) let candidates: Vec = follows.iter().map(|f| f.did.clone()).collect(); let played = match state.db.played_dids(&candidates).await { Ok(played) => played, - Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + Err(_) => { + tracing::error!(did, "opponents: played-dids lookup failed"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } }; let faced = match state.db.all_opponent_dids(&did).await { Ok(faced) => faced, - Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + Err(_) => { + tracing::error!(did, "opponents: all-opponent-dids lookup failed"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } }; let suggested: Vec<_> = follows @@ -1070,7 +1089,10 @@ async fn list_matches(State(state): State, jar: CookieJar) -> Response }; let rows = match state.db.list_matches(&did, 20).await { Ok(rows) => rows, - Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + Err(_) => { + tracing::error!(did, "matches: list lookup failed"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } }; let mut matches = Vec::with_capacity(rows.len()); for row in rows { @@ -1114,7 +1136,10 @@ async fn match_status( Ok(Some(row)) => row, // Not yours reads as not found - no existence oracle. Ok(None) => return StatusCode::NOT_FOUND.into_response(), - Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), + Err(_) => { + tracing::error!(did, match_id = id, "match status: match lookup failed"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } }; // A seat is enough: an invited player polls the same waiting screen the // launcher does. -- 2.51.2