diff --git a/internal/api/admin_cover_sources.go b/internal/api/admin_cover_sources.go index 28d6bafd..5c9c1171 100644 --- a/internal/api/admin_cover_sources.go +++ b/internal/api/admin_cover_sources.go @@ -90,7 +90,7 @@ func (h *handlers) handleUpdateCoverSource(w http.ResponseWriter, r *http.Reques return } h.logger.Error("admin: update cover source", "id", id, "err", err) - writeErr(w, apierror.Internal(err)) + writeErr(w, apierror.InternalMsg("update failed", err)) return } diff --git a/internal/api/admin_coverage.go b/internal/api/admin_coverage.go index cf762ae1..422dcc4d 100644 --- a/internal/api/admin_coverage.go +++ b/internal/api/admin_coverage.go @@ -25,7 +25,7 @@ func (h *handlers) handleGetLibraryCoverage(w http.ResponseWriter, r *http.Reque q := dbq.New(h.pool) row, err := q.GetAlbumCoverageRollup(r.Context()) if err != nil { - writeErrWithLog(w, h.logger, "admin: get coverage rollup", apierror.Internal(err)) + writeErrWithLog(w, h.logger, "admin: get coverage rollup", apierror.InternalMsg("lookup failed", err)) return } writeJSON(w, http.StatusOK, coverageRollupResp{ diff --git a/internal/api/admin_coverart_research.go b/internal/api/admin_coverart_research.go index bcf97193..c945880b 100644 --- a/internal/api/admin_coverart_research.go +++ b/internal/api/admin_coverart_research.go @@ -18,7 +18,7 @@ import ( func (h *handlers) handleResearchMissingArt(w http.ResponseWriter, r *http.Request) { newVer, err := h.coverSettings.BumpVersion(r.Context()) if err != nil { - writeErrWithLog(w, h.logger, "admin: research missing art bump failed", apierror.Internal(err)) + writeErrWithLog(w, h.logger, "admin: research missing art bump failed", apierror.InternalMsg("bump failed", err)) return } writeJSON(w, http.StatusOK, map[string]any{"version": newVer}) diff --git a/internal/api/admin_covers.go b/internal/api/admin_covers.go index 84dfa979..df9c8e18 100644 --- a/internal/api/admin_covers.go +++ b/internal/api/admin_covers.go @@ -32,12 +32,12 @@ func (h *handlers) handleAdminAlbumRefetchCover(w http.ResponseWriter, r *http.R writeErr(w, &apierror.Error{Status: http.StatusNotFound, Code: "not_found", Message: "album not found"}) return } - writeErrWithLog(w, h.logger, "admin: cover refetch failed", apierror.Internal(err)) + writeErrWithLog(w, h.logger, "admin: cover refetch failed", apierror.InternalMsg("refetch failed", err)) return } row, err := dbq.New(h.pool).GetAlbumWithFirstTrackPath(r.Context(), albumID) if err != nil { - writeErrWithLog(w, h.logger, "admin: post-refetch read failed", apierror.Internal(err)) + writeErrWithLog(w, h.logger, "admin: post-refetch read failed", apierror.InternalMsg("read failed", err)) return } writeJSON(w, http.StatusOK, adminCoverRefetchResp{ diff --git a/internal/api/admin_scan.go b/internal/api/admin_scan.go index f9859f50..c2b5854e 100644 --- a/internal/api/admin_scan.go +++ b/internal/api/admin_scan.go @@ -40,7 +40,7 @@ func (h *handlers) handleGetScanStatus(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, scanStatusResp{InFlight: false}) return } - writeErrWithLog(w, h.logger, "admin: get scan status", apierror.Internal(err)) + writeErrWithLog(w, h.logger, "admin: get scan status", apierror.InternalMsg("lookup failed", err)) return } @@ -85,7 +85,7 @@ func (h *handlers) handleTriggerScan(w http.ResponseWriter, _ *http.Request) { h.logger.With("source", "manual"), h.scanCfg, ) if err != nil { - writeErrWithLog(w, h.logger, "admin: trigger scan failed", apierror.Internal(err)) + writeErrWithLog(w, h.logger, "admin: trigger scan failed", apierror.InternalMsg("trigger failed", err)) return } if !started { diff --git a/internal/api/admin_scan_schedule.go b/internal/api/admin_scan_schedule.go index 4727fe36..539b6d60 100644 --- a/internal/api/admin_scan_schedule.go +++ b/internal/api/admin_scan_schedule.go @@ -35,10 +35,10 @@ func (h *handlers) handleGetScanSchedule(w http.ResponseWriter, r *http.Request) if err != nil { if errors.Is(err, pgx.ErrNoRows) { h.logger.Error("admin: scan_schedule singleton row missing") - writeErr(w, apierror.Internal(errors.New("schedule row missing"))) + writeErr(w, apierror.InternalMsg("schedule row missing", errors.New("schedule row missing"))) return } - writeErrWithLog(w, h.logger, "admin: get scan schedule", apierror.Internal(err)) + writeErrWithLog(w, h.logger, "admin: get scan schedule", apierror.InternalMsg("lookup failed", err)) return } @@ -64,7 +64,7 @@ func (h *handlers) handlePatchScanSchedule(w http.ResponseWriter, r *http.Reques TimeOfDay: req.TimeOfDay, WeeklyDay: pgInt32Ptr(req.WeeklyDay), }); err != nil { - writeErrWithLog(w, h.logger, "admin: update scan schedule", apierror.Internal(err)) + writeErrWithLog(w, h.logger, "admin: update scan schedule", apierror.InternalMsg("update failed", err)) return } @@ -72,7 +72,7 @@ func (h *handlers) handlePatchScanSchedule(w http.ResponseWriter, r *http.Reques row, err := q.GetScanSchedule(r.Context()) if err != nil { - writeErrWithLog(w, h.logger, "admin: re-read scan schedule", apierror.Internal(err)) + writeErrWithLog(w, h.logger, "admin: re-read scan schedule", apierror.InternalMsg("re-read failed", err)) return } cfg := library.ScheduleConfig{Mode: row.Mode} diff --git a/internal/api/admin_tracks.go b/internal/api/admin_tracks.go index 8dca2780..c39df60e 100644 --- a/internal/api/admin_tracks.go +++ b/internal/api/admin_tracks.go @@ -69,7 +69,7 @@ func (h *handlers) handleRemoveTrack(w http.ResponseWriter, r *http.Request) { return } h.logger.Error("api: remove track failed", "err", err, "track_id", idStr) - writeErr(w, apierror.Internal(err)) + writeErr(w, apierror.InternalMsg("remove failed", err)) return } diff --git a/internal/apierror/apierror.go b/internal/apierror/apierror.go index 6ee53801..3b876499 100644 --- a/internal/apierror/apierror.go +++ b/internal/apierror/apierror.go @@ -58,6 +58,14 @@ func Internal(cause error) *Error { return &Error{Status: 500, Code: "server_error", Message: "internal server error", Cause: cause} } +// InternalMsg returns a 500 Error with a custom user-facing message +// and a cause for logging. Use when the call site has a meaningful +// message that should appear on the wire (e.g. "lookup failed") rather +// than the generic "internal server error" from Internal. +func InternalMsg(message string, cause error) *Error { + return &Error{Status: 500, Code: "server_error", Message: message, Cause: cause} +} + var ( ErrNotFound = &Error{Status: 404, Code: "not_found", Message: "not found"} ErrForbidden = &Error{Status: 403, Code: "forbidden", Message: "forbidden"} diff --git a/internal/apierror/apierror_test.go b/internal/apierror/apierror_test.go index 1a0b212f..1d8ac0f9 100644 --- a/internal/apierror/apierror_test.go +++ b/internal/apierror/apierror_test.go @@ -143,6 +143,23 @@ func TestConstructors_FieldsCorrect(t *testing.T) { } } +func TestInternalMsg(t *testing.T) { + cause := errors.New("db down") + e := InternalMsg("lookup failed", cause) + if e.Status != 500 { + t.Errorf("Status = %d, want 500", e.Status) + } + if e.Code != "server_error" { + t.Errorf("Code = %q, want server_error", e.Code) + } + if e.Message != "lookup failed" { + t.Errorf("Message = %q, want lookup failed", e.Message) + } + if !errors.Is(e, cause) { + t.Errorf("expected Is(cause) to be true") + } +} + func TestSentinels_Identity(t *testing.T) { wrapped := fmt.Errorf("ctx: %w", ErrNotFound)