package apierrors import ( stdErrors "errors" "net/http" ) // APIError is the public error shape used by handlers and middleware. // Cause is retained for logging and unwrapping; default response encoders and // replay snapshots never serialize it. type APIError struct { Status int Code string Message string Cause error BusinessCode any // Optional public code resolved from a project catalog. reference bool params map[string]any paramErr error } func (e *APIError) Error() string { if e == nil { return "" } if e.Cause != nil { return e.Code + ": " + e.Message + ": " + e.Cause.Error() } return e.Code + ": " + e.Message } func (e *APIError) Unwrap() error { if e == nil { return nil } return e.Cause } // WithCause attaches an internal cause while keeping the public response stable. func (e *APIError) WithCause(cause error) *APIError { e.Cause = cause return e } func New(status int, code, message string) *APIError { return &APIError{Status: status, Code: code, Message: message} } func BadRequest(code, message string) *APIError { return New(http.StatusBadRequest, code, message) } func Unauthorized(code, message string) *APIError { return New(http.StatusUnauthorized, code, message) } func Forbidden(code, message string) *APIError { return New(http.StatusForbidden, code, message) } func NotFound(code, message string) *APIError { return New(http.StatusNotFound, code, message) } func Internal(cause error) *APIError { return New(http.StatusInternalServerError, "INTERNAL_ERROR", "internal server error").WithCause(cause) } // From converts any error into the public API error shape. func From(err error) *APIError { if err == nil { return nil } var apiErr *APIError if stdErrors.As(err, &apiErr) && apiErr != nil { if apiErr.reference { return Internal(err) } return apiErr } return Internal(err) }