mercury/web/api/error.go

190 lines
4.7 KiB
Go

package api
import (
"fmt"
"net/http"
"emperror.dev/errors"
"github.com/go-chi/render"
)
// NoValue should be returned by functions that either:
// - don't return any content for one or more code paths
// - handle rendering content themselves
const NoValue = errors.Sentinel("no content")
// WrapHandlerT wraps a modified http.HandlerFunc into a stdlib-compatible one.
// The inner HandlerFunc additionally returns a JSON response and an error.
func WrapHandlerT[T any](hn func(w http.ResponseWriter, r *http.Request) (T, error)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
v, err := hn(w, r)
if err == nil {
render.JSON(w, r, v)
return
} else if err == NoValue {
return
}
// if the function returned an API error, just render that verbatim
if apiErr, ok := err.(Error); ok {
apiErr.prepare()
render.Status(r, apiErr.Status)
render.JSON(w, r, apiErr)
return
}
// otherwise, we return an internal server error message
apiErr := Error{Code: ErrInternalServerError}
apiErr.prepare()
render.Status(r, apiErr.Status)
render.JSON(w, r, apiErr)
}
}
// WrapHandler wraps a modified http.HandlerFunc into a stdlib-compatible one.
// The inner HandlerFunc additionally returns an error.
func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
fn := func(w http.ResponseWriter, r *http.Request) (any, error) {
err := hn(w, r)
if err == nil {
return nil, NoValue
}
return nil, err
}
return WrapHandlerT(fn)
// return func(w http.ResponseWriter, r *http.Request) {
// err := hn(w, r)
// if err != nil {
// // if the function returned an API error, just render that verbatim
// // we can assume that it also logged the error (if that was needed)
// if apiErr, ok := err.(Error); ok {
// apiErr.prepare()
// render.Status(r, apiErr.Status)
// render.JSON(w, r, apiErr)
// return
// }
// // otherwise, we log the error and return an internal server error message
// log.Err(err).Msg("error in http handler")
// apiErr := Error{Code: ErrInternalServerError}
// apiErr.prepare()
// render.Status(r, apiErr.Status)
// render.JSON(w, r, apiErr)
// }
// }
}
type Error struct {
Code int `json:"code"`
Message string `json:"message,omitempty"`
Details string `json:"details,omitempty"`
// Status is set as the HTTP status code.
Status int `json:"-"`
}
func (e Error) Error() string {
if e.Message == "" {
e.Message = errCodeMessages[e.Code]
}
if e.Details != "" {
return fmt.Sprintf("%s (code: %d) (%s)", e.Message, e.Code, e.Details)
}
return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
}
func (e *Error) prepare() {
if e.Status == 0 {
e.Status = errCodeStatuses[e.Code]
}
if e.Message == "" {
e.Message = errCodeMessages[e.Code]
}
}
// Error code constants
const (
ErrBadRequest = 400
ErrForbidden = 403
ErrNotFound = 404
ErrMethodNotAllowed = 405
ErrTooManyRequests = 429
ErrInternalServerError = 500 // catch-all code for unknown errors
// Auth related
ErrInvalidToken = 1001
ErrMissingScope = 1002
ErrNotYourObject = 1003
// Account related
ErrAccountNotFound = 2001
// Blog related
ErrBlogNotFound = 3001
// Post related
ErrPostNotFound = 4001
// Streaming related
ErrTooManyStreams = 5001
)
func ErrCodeMessage(code int) string {
return errCodeMessages[code]
}
var errCodeMessages = map[int]string{
ErrBadRequest: "Bad request",
ErrForbidden: "Forbidden",
ErrInternalServerError: "Internal server error",
ErrNotFound: "Not found",
ErrTooManyRequests: "Rate limit reached",
ErrMethodNotAllowed: "Method not allowed",
ErrInvalidToken: "No token supplied, or token is invalid",
ErrMissingScope: "Token is missing required scope for this endpoint",
ErrNotYourObject: "Object you are trying to perform action on is not owned by you",
ErrAccountNotFound: "Account not found",
ErrBlogNotFound: "Blog not found",
ErrPostNotFound: "Post not found",
ErrTooManyStreams: "Too many streams open",
}
func ErrCodeStatus(code int) int {
return errCodeStatuses[code]
}
var errCodeStatuses = map[int]int{
ErrBadRequest: http.StatusBadRequest,
ErrForbidden: http.StatusForbidden,
ErrInternalServerError: http.StatusInternalServerError,
ErrNotFound: http.StatusNotFound,
ErrTooManyRequests: http.StatusTooManyRequests,
ErrMethodNotAllowed: http.StatusMethodNotAllowed,
ErrInvalidToken: http.StatusUnauthorized,
ErrMissingScope: http.StatusForbidden,
ErrNotYourObject: http.StatusForbidden,
ErrAccountNotFound: http.StatusNotFound,
ErrBlogNotFound: http.StatusNotFound,
ErrPostNotFound: http.StatusNotFound,
ErrTooManyStreams: http.StatusBadRequest,
}