package api import ( "crypto/rand" "encoding/hex" "errors" "fmt" "log/slog" "net/http" "github.com/gin-gonic/gin" "github.com/dhao2001/mygo/internal/model" ) // ErrorResponse is the standard JSON body for HTTP API errors. type ErrorResponse struct { Error ErrorBody `json:"error"` } // ErrorBody contains human-readable error details. type ErrorBody struct { Message string `json:"message"` } // Error writes a JSON error response. func Error(c *gin.Context, status int, message string) { c.JSON(status, ErrorResponse{ Error: ErrorBody{ Message: message, }, }) } // RespondError unpacks an error from the service layer and writes a JSON // error response. It expects *model.AppError; unexpected non-AppError // values are treated as 500 with a safe message. For 500+ errors, a // random reference hash is included in the response so operators can // correlate it with server logs. func RespondError(c *gin.Context, err error) { var ae *model.AppError if !errors.As(err, &ae) { ref := randomHex(8) slog.ErrorContext(c.Request.Context(), "non-AppError escaped to handler", "ref", ref, "error", err, "type", fmt.Sprintf("%T", err)) Error(c, http.StatusInternalServerError, "internal server error (ref: "+ref+")") return } // 500+ errors: log full detail with a reference hash, return sanitized message. if ae.Status >= 500 { ref := randomHex(8) slog.ErrorContext(c.Request.Context(), "internal server error", "ref", ref, slog.Any("error", ae.Err), "message", ae.Message) Error(c, ae.Status, "internal server error (ref: "+ref+")") return } // 4xx errors with internal cause: log at WARN for diagnostics. if ae.Err != nil { slog.WarnContext(c.Request.Context(), ae.Message, "status", ae.Status, slog.Any("error", ae.Err)) } Error(c, ae.Status, ae.Message) } func randomHex(n int) string { b := make([]byte, n) rand.Read(b) return hex.EncodeToString(b) }