Files
mygo/internal/api/response.go
T
ld 1dfccf513a Add structured logging and centralized error handling
- 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`
2026-07-04 16:24:22 +08:00

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)
}