Files
minstrel/internal/api/errors.go
T
bvandeusenandClaude Opus 5.5 08a2c06b85 docs: api comments say what the endpoints do, not how they came to be (#6277)
Rewrites the internal/api comment blocks to rule 32: task, milestone and
rule ids gone, the stories behind them restated as the constraint they
left, stale references corrected (the stream secret is wired from config,
the timezone handler already refreshes the scheduler, the Android app is
the native client, the design docs cited are not in the repo). Also
rewraps comment lines over 80 columns left by the earlier batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:23:58 -04:00

79 lines
2.4 KiB
Go

package api
import (
"encoding/json"
"log/slog"
"math"
"net/http"
"strconv"
"time"
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
)
// errorBody / errorPayload preserve the existing wire envelope:
//
// {"error": {"code": "...", "message": "..."}}
//
// Kept separate from the Subsonic envelope so native clients don't have
// to parse two shapes.
type errorBody struct {
Error errorPayload `json:"error"`
}
type errorPayload struct {
Code string `json:"code"`
Message string `json:"message"`
}
// writeErr emits the failure envelope for err. If err is or wraps
// *apierror.Error, its Status/Code/Message drive the response;
// otherwise it's mapped to a 500 server_error.
func writeErr(w http.ResponseWriter, err error) {
apiErr := apierror.From(err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(apiErr.Status)
_ = json.NewEncoder(w).Encode(errorBody{Error: errorPayload{
Code: apiErr.Code,
Message: apiErr.Message,
}})
}
// writeRateLimited answers 429 with Retry-After in whole seconds, rounded
// up so a client that honours it never arrives a moment early.
func writeRateLimited(w http.ResponseWriter, wait time.Duration) {
secs := int(math.Ceil(wait.Seconds()))
if secs < 1 {
secs = 1
}
w.Header().Set("Retry-After", strconv.Itoa(secs))
writeErr(w, apierror.TooManyRequests("rate_limited", "too many attempts; try again later"))
}
// writeErrWithLog logs the error at Error level and writes the response.
// Use for 500-class errors whose cause belongs in the logs.
func writeErrWithLog(w http.ResponseWriter, logger *slog.Logger, msg string, err error) {
if logger != nil {
logger.Error(msg, "err", err)
}
writeErr(w, err)
}
// writeJSON emits a 2xx response with the given body JSON-encoded. Mirrors
// writeErr's shape so handlers have one shape for success and one for
// failure.
func writeJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
}
// writeAdminJSONErr writes a flat {"error":"<code>"} JSON response. This
// mirrors the shape the RequireAdmin middleware uses and that the spec
// defines for /api/admin/* errors.
func writeAdminJSONErr(w http.ResponseWriter, status int, code string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": code})
}