Newer
Older
minecraft-ui / internal / rest / error.go
package rest

import (
	"net/http"
	"fmt"
)

// InternalError defines an error not meant to be exposed to the public.
const (
	InternalErrorCode   = -1
	InternalErrorStatus = "internal_error"
)

// Enumeration of generic errors
var (
	ErrNotFound             = E(http.StatusNotFound, "not_found")(nil)
	ErrUnsupportedMediaType = E(http.StatusUnsupportedMediaType, "unsupported_media_type")(nil)

	ErrInternalServerError = E(http.StatusInternalServerError, "internal_server_error")
	ErrMalformedPayload    = E(http.StatusBadRequest, "malformed_payload")
)

// E is a convenience functions to construct a REST error which takes an error.
func E(code int, status string) func(err error) Error {
	return func(err error) Error { return NewError(code, status, err) }
}

// NewError returns an error meant to rendered as a REST response with a given
// HTTP status code and JSON body.
func NewError(code int, status string, err error, opts ...Option) Error {
	var msg string
	if err != nil {
		msg = err.Error()
	}

	e := Error{
		Code:    code,
		Status:  status,
		Message: msg,
		Errors:  UnwrapError(err),
	}

	for _, o := range opts {
		o(&e)
	}

	// On internal server errors, we don't want to expose the internals via the
	// `Errors` field.
	if status == "" {
		e.Code = http.StatusInternalServerError
		e.Message = http.StatusText(http.StatusInternalServerError)
	}

	return e
}

// Error defines a failed request's response result.
//
// It vaguely follows: [Google Cloud APIs Errors](https://cloud.google.com/apis/design/errors)
type Error struct {
	Code    int         `json:"code,omitempty"`    // HTTP status code mapping, optional for nestings
	Status  string      `json:"status"`            // More granular code, snake-cases, unique per service
	Message string      `json:"message,omitempty"` // Human readable
	Meta    interface{} `json:"meta,omitempty"`    // Meta information
	Errors  []Error     `json:"errors,omitempty"`  // Recursive
}

// Error implements the error interface.
func (e Error) Error() string {
	return fmt.Sprintf("%d %s: %s: %v", e.Code, e.Status, e.Message, e.Errors)
}

// Option defines an option to a new error.
type Option func(*Error)

// WithMeta attaches meta information to an error.
func WithMeta(meta interface{}) func(*Error) {
	return func(e *Error) {
		e.Meta = meta
	}
}

// WithErrors attaches a sequence of errors as children to a parent error.
func WithErrors(errs []Error) func(*Error) {
	return func(e *Error) {
		e.Errors = append(e.Errors, errs...)
	}
}

// UnwrapError unwraps wrapped errors until either nil or an unwrappable error
// is found.
//
// Modified version of https://github.com/pkg/errors/blob/master/errors.go#L256
func UnwrapError(err error) []Error {
	type causer interface {
		Cause() error
	}

	errs := []Error{}

	if cause, ok := err.(causer); ok {
		err = cause.Cause()
	} else {
		err = nil
	}

	for err != nil {
		if err, ok := err.(Error); ok {
			errs = append(errs, NewError(err.Code, err.Status, err))
		} else {
			errs = append(errs, NewError(InternalErrorCode, InternalErrorStatus, err))
		}

		cause, ok := err.(causer)
		if !ok {
			break
		}
		err = cause.Cause()
	}

	return errs
}