1dfccf513a
- Initialize slog in the serve command with terminal/file support - Introduce `AppError` with HTTP status for unified service-layer errors - Replace ad-hoc `api.Error` calls with `api.RespondError` - Wrap internal errors with `model.NewInternalError` and add reference IDs - Add `RequestID` middleware and switch router from `gin.Default` to `gin.New`
73 lines
1.9 KiB
Go
73 lines
1.9 KiB
Go
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)
|
|
}
|