Skip to content

Commit b136ac2

Browse files
julianknutsenclaude
andcommitted
fix: sanitize upstream errors in dashboard/leaderboard and stop frontend 503 double-reporting
handleDashboard and handleLeaderboard were returning raw DoltHub error details as 500s — leaking SQL queries and not triggering re-auth for expired tokens. Now uses writeUpstreamError() to classify: auth failures → 401, upstream outages → 503 with sanitized message. Frontend no longer captures 503s to Sentry since they are intentional "upstream unavailable" responses already captured server-side. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d9ea849 commit b136ac2

2 files changed

Lines changed: 18 additions & 3 deletions

File tree

internal/api/handlers.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
143143
}
144144
data, err := client.Dashboard()
145145
if err != nil {
146-
writeError(w, http.StatusInternalServerError, err.Error())
146+
writeUpstreamError(w, err, "dashboard")
147147
return
148148
}
149149
writeJSON(w, http.StatusOK, toDashboardResponse(data))
@@ -157,7 +157,7 @@ func (s *Server) handleLeaderboard(w http.ResponseWriter, r *http.Request) {
157157
limit := parseIntParam(r, "limit", 20)
158158
entries, err := client.Leaderboard(limit)
159159
if err != nil {
160-
writeError(w, http.StatusInternalServerError, err.Error())
160+
writeUpstreamError(w, err, "leaderboard")
161161
return
162162
}
163163
writeJSON(w, http.StatusOK, toLeaderboardResponse(entries))
@@ -227,6 +227,21 @@ func (s *Server) invalidateAllCaches() {
227227
s.detailCache.Invalidate()
228228
}
229229

230+
// writeUpstreamError classifies DoltHub errors and writes an appropriate response:
231+
// - "invalid authorization" → 401 (triggers frontend re-auth)
232+
// - other upstream errors → 503 with sanitized message + Sentry capture
233+
func writeUpstreamError(w http.ResponseWriter, err error, label string) {
234+
msg := err.Error()
235+
if strings.Contains(msg, "invalid authorization") {
236+
writeError(w, http.StatusUnauthorized, "DoltHub credentials expired — please reconnect.")
237+
return
238+
}
239+
slog.Error(label+" failed", "error", err)
240+
sentry.CaptureException(err)
241+
writeError(w, http.StatusServiceUnavailable,
242+
"Upstream database is temporarily unavailable — please try again in a moment.")
243+
}
244+
230245
// writeMutationError writes a 409 for ConflictError, 400 for everything else.
231246
func writeMutationError(w http.ResponseWriter, err error) {
232247
var conflict *commons.ConflictError

web/src/api/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
125125
}
126126
if (!resp.ok) {
127127
const err = new ApiError(resp.status, (body as ErrorResponse).error || resp.statusText);
128-
if (resp.status >= 500) {
128+
if (resp.status >= 500 && resp.status !== 503) {
129129
Sentry.captureException(err);
130130
}
131131
throw err;

0 commit comments

Comments
 (0)