pronounsfu/backend/server/errors.go

202 lines
7.1 KiB
Go

package server
import (
"fmt"
"net/http"
"codeberg.org/u1f320/pronouns.cc/backend/log"
"github.com/go-chi/render"
)
// 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 {
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.(APIError); 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.Errorf("error in http handler: %v", err)
apiErr := APIError{Code: ErrInternalServerError}
apiErr.prepare()
render.Status(r, apiErr.Status)
render.JSON(w, r, apiErr)
}
}
}
// APIError is an object returned by the API when an error occurs.
// It implements the error interface and can be returned by handlers.
type APIError struct {
Code int `json:"code"`
Message string `json:"message,omitempty"`
Details string `json:"details,omitempty"`
RatelimitReset *int `json:"ratelimit_reset,omitempty"`
// Status is set as the HTTP status code.
Status int `json:"-"`
}
func (e APIError) 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 *APIError) 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
// Login/authorize error codes
ErrInvalidState = 1001
ErrInvalidOAuthCode = 1002
ErrInvalidToken = 1003 // a token was supplied, but it is invalid
ErrInviteRequired = 1004
ErrInvalidTicket = 1005 // invalid signup ticket
ErrInvalidUsername = 1006 // invalid username (when signing up)
ErrUsernameTaken = 1007 // username taken (when signing up)
ErrInvitesDisabled = 1008 // invites are disabled (unneeded)
ErrInviteLimitReached = 1009 // invite limit reached (when creating invites)
ErrInviteAlreadyUsed = 1010 // invite already used (when signing up)
ErrDeletionPending = 1011 // own user deletion pending, returned with undo code
ErrRecentExport = 1012 // latest export is too recent
ErrUnsupportedInstance = 1013 // unsupported fediverse software
ErrAlreadyLinked = 1014 // user already has linked account of the same type
ErrNotLinked = 1015 // user already doesn't have a linked account
ErrLastProvider = 1016 // unlinking provider would leave account with no authentication method
ErrInvalidCaptcha = 1017 // invalid or missing captcha response
// User-related error codes
ErrUserNotFound = 2001
ErrMemberListPrivate = 2002
// Member-related error codes
ErrMemberNotFound = 3001
ErrMemberLimitReached = 3002
ErrMemberNameInUse = 3003
ErrNotOwnMember = 3004
// General request error codes
ErrRequestTooBig = 4001
ErrMissingPermissions = 4002
// Moderation related error codes
ErrReportAlreadyHandled = 5001
ErrNotSelfDelete = 5002
)
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",
ErrInvalidState: "Invalid OAuth state",
ErrInvalidOAuthCode: "Invalid OAuth code",
ErrInvalidToken: "Supplied token was invalid",
ErrInviteRequired: "A valid invite code is required",
ErrInvalidTicket: "Invalid signup ticket",
ErrInvalidUsername: "Invalid username",
ErrUsernameTaken: "Username is already taken",
ErrInvitesDisabled: "Invites are disabled",
ErrInviteLimitReached: "Your account has reached the invite limit",
ErrInviteAlreadyUsed: "That invite code has already been used",
ErrDeletionPending: "Your account is pending deletion",
ErrRecentExport: "Your latest data export is less than 1 day old",
ErrUnsupportedInstance: "Unsupported instance software",
ErrAlreadyLinked: "Your account is already linked to an account of this type",
ErrNotLinked: "Your account is already not linked to an account of this type",
ErrLastProvider: "This is your account's only authentication provider",
ErrInvalidCaptcha: "Invalid or missing captcha response",
ErrUserNotFound: "User not found",
ErrMemberListPrivate: "This user's member list is private.",
ErrMemberNotFound: "Member not found",
ErrMemberLimitReached: "Member limit reached",
ErrMemberNameInUse: "Member name already in use",
ErrNotOwnMember: "Not your member",
ErrRequestTooBig: "Request too big (max 2 MB)",
ErrMissingPermissions: "Your account or current token is missing required permissions for this action",
ErrReportAlreadyHandled: "Report has already been resolved",
ErrNotSelfDelete: "Cannot cancel deletion for an account deleted by a moderator",
}
var errCodeStatuses = map[int]int{
ErrBadRequest: http.StatusBadRequest,
ErrForbidden: http.StatusForbidden,
ErrInternalServerError: http.StatusInternalServerError,
ErrNotFound: http.StatusNotFound,
ErrTooManyRequests: http.StatusTooManyRequests,
ErrMethodNotAllowed: http.StatusMethodNotAllowed,
ErrInvalidState: http.StatusBadRequest,
ErrInvalidOAuthCode: http.StatusForbidden,
ErrInvalidToken: http.StatusUnauthorized,
ErrInviteRequired: http.StatusBadRequest,
ErrInvalidTicket: http.StatusBadRequest,
ErrInvalidUsername: http.StatusBadRequest,
ErrUsernameTaken: http.StatusBadRequest,
ErrInvitesDisabled: http.StatusForbidden,
ErrInviteLimitReached: http.StatusForbidden,
ErrInviteAlreadyUsed: http.StatusBadRequest,
ErrDeletionPending: http.StatusBadRequest,
ErrRecentExport: http.StatusBadRequest,
ErrUnsupportedInstance: http.StatusBadRequest,
ErrAlreadyLinked: http.StatusBadRequest,
ErrNotLinked: http.StatusBadRequest,
ErrLastProvider: http.StatusBadRequest,
ErrInvalidCaptcha: http.StatusBadRequest,
ErrUserNotFound: http.StatusNotFound,
ErrMemberListPrivate: http.StatusForbidden,
ErrMemberNotFound: http.StatusNotFound,
ErrMemberLimitReached: http.StatusBadRequest,
ErrMemberNameInUse: http.StatusBadRequest,
ErrNotOwnMember: http.StatusForbidden,
ErrRequestTooBig: http.StatusBadRequest,
ErrMissingPermissions: http.StatusForbidden,
ErrReportAlreadyHandled: http.StatusBadRequest,
ErrNotSelfDelete: http.StatusForbidden,
}