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