route/vendor/github.com/twitchtv/twirp/errors.go

329 lines
11 KiB
Go

// package twirp provides core types used in generated Twirp servers and client.
//
// Twirp services handle errors using the `twirp.Error` interface.
//
// For example, a server method may return an InvalidArgumentError:
//
// if req.Order != "DESC" && req.Order != "ASC" {
// return nil, twirp.InvalidArgumentError("Order", "must be DESC or ASC")
// }
//
// And the same twirp.Error is returned by the client, for example:
//
// resp, err := twirpClient.RPCMethod(ctx, req)
// if err != nil {
// if twerr := err.(twirp.Error) {
// switch twerr.Code() {
// case twirp.InvalidArgument:
// log.Error("invalid argument "+twirp.Meta("argument"))
// default:
// log.Error(twerr.Error())
// }
// }
// }
//
// Clients may also return Internal errors if something failed on the system:
// the server, the network, or the client itself (i.e. failure parsing
// response).
//
package twirp
import "fmt"
// Error represents an error in a Twirp service call.
type Error interface {
// Code is of the valid error codes.
Code() ErrorCode
// Msg returns a human-readable, unstructured messages describing the error.
Msg() string
// WithMeta returns a copy of the Error with the given key-value pair attached
// as metadata. If the key is already set, it is overwritten.
WithMeta(key string, val string) Error
// Meta returns the stored value for the given key. If the key has no set
// value, Meta returns an empty string. There is no way to distinguish between
// an unset value and an explicit empty string.
Meta(key string) string
// MetaMap returns the complete key-value metadata map stored on the error.
MetaMap() map[string]string
// Error returns a string of the form "twirp error <Type>: <Msg>"
Error() string
}
// NewError is the generic constructor for a twirp.Error. The ErrorCode must be
// one of the valid predefined constants, otherwise it will be converted to an
// error {type: Internal, msg: "invalid error type {{code}}"}. If you need to
// add metadata, use .WithMeta(key, value) method after building the error.
func NewError(code ErrorCode, msg string) Error {
if IsValidErrorCode(code) {
return &twerr{
code: code,
msg: msg,
}
}
return &twerr{
code: Internal,
msg: "invalid error type " + string(code),
}
}
// NotFoundError constructor for the common NotFound error.
func NotFoundError(msg string) Error {
return NewError(NotFound, msg)
}
// InvalidArgumentError constructor for the common InvalidArgument error. Can be
// used when an argument has invalid format, is a number out of range, is a bad
// option, etc).
func InvalidArgumentError(argument string, validationMsg string) Error {
err := NewError(InvalidArgument, argument+" "+validationMsg)
err = err.WithMeta("argument", argument)
return err
}
// RequiredArgumentError is a more scpecific constructor for InvalidArgument
// error. Should be used when the argument is required (expected to have a
// non-zero value).
func RequiredArgumentError(argument string) Error {
return InvalidArgumentError(argument, "is required")
}
// InternalError constructor for the common Internal error. Should be used to
// specify that something bad or unexpected happened.
func InternalError(msg string) Error {
return NewError(Internal, msg)
}
// InternalErrorWith is an easy way to wrap another error. It adds the
// underlying error's type as metadata with a key of "cause", which can be
// useful for debugging. Should be used in the common case of an unexpected
// error returned from another API, but sometimes it is better to build a more
// specific error (like with NewError(Unknown, err.Error()), for example).
//
// The returned error also has a Cause() method which will return the original
// error, if it is known. This can be used with the github.com/pkg/errors
// package to extract the root cause of an error. Information about the root
// cause of an error is lost when it is serialized, so this doesn't let a client
// know the exact root cause of a server's error.
func InternalErrorWith(err error) Error {
msg := err.Error()
twerr := NewError(Internal, msg)
twerr = twerr.WithMeta("cause", fmt.Sprintf("%T", err)) // to easily tell apart wrapped internal errors from explicit ones
return &wrappedErr{
wrapper: twerr,
cause: err,
}
}
// ErrorCode represents a Twirp error type.
type ErrorCode string
// Valid Twirp error types. Most error types are equivalent to GRPC status codes
// and follow the same semantics.
const (
// Canceled indicates the operation was cancelled (typically by the caller).
Canceled ErrorCode = "canceled"
// Unknown error. For example when handling errors raised by APIs that do not
// return enough error information.
Unknown ErrorCode = "unknown"
// InvalidArgument indicates client specified an invalid argument. It
// indicates arguments that are problematic regardless of the state of the
// system (i.e. a malformed file name, required argument, number out of range,
// etc.).
InvalidArgument ErrorCode = "invalid_argument"
// DeadlineExceeded means operation expired before completion. For operations
// that change the state of the system, this error may be returned even if the
// operation has completed successfully (timeout).
DeadlineExceeded ErrorCode = "deadline_exceeded"
// NotFound means some requested entity was not found.
NotFound ErrorCode = "not_found"
// BadRoute means that the requested URL path wasn't routable to a Twirp
// service and method. This is returned by the generated server, and usually
// shouldn't be returned by applications. Instead, applications should use
// NotFound or Unimplemented.
BadRoute ErrorCode = "bad_route"
// AlreadyExists means an attempt to create an entity failed because one
// already exists.
AlreadyExists ErrorCode = "already_exists"
// PermissionDenied indicates the caller does not have permission to execute
// the specified operation. It must not be used if the caller cannot be
// identified (Unauthenticated).
PermissionDenied ErrorCode = "permission_denied"
// Unauthenticated indicates the request does not have valid authentication
// credentials for the operation.
Unauthenticated ErrorCode = "unauthenticated"
// ResourceExhausted indicates some resource has been exhausted, perhaps a
// per-user quota, or perhaps the entire file system is out of space.
ResourceExhausted ErrorCode = "resource_exhausted"
// FailedPrecondition indicates operation was rejected because the system is
// not in a state required for the operation's execution. For example, doing
// an rmdir operation on a directory that is non-empty, or on a non-directory
// object, or when having conflicting read-modify-write on the same resource.
FailedPrecondition ErrorCode = "failed_precondition"
// Aborted indicates the operation was aborted, typically due to a concurrency
// issue like sequencer check failures, transaction aborts, etc.
Aborted ErrorCode = "aborted"
// OutOfRange means operation was attempted past the valid range. For example,
// seeking or reading past end of a paginated collection.
//
// Unlike InvalidArgument, this error indicates a problem that may be fixed if
// the system state changes (i.e. adding more items to the collection).
//
// There is a fair bit of overlap between FailedPrecondition and OutOfRange.
// We recommend using OutOfRange (the more specific error) when it applies so
// that callers who are iterating through a space can easily look for an
// OutOfRange error to detect when they are done.
OutOfRange ErrorCode = "out_of_range"
// Unimplemented indicates operation is not implemented or not
// supported/enabled in this service.
Unimplemented ErrorCode = "unimplemented"
// Internal errors. When some invariants expected by the underlying system
// have been broken. In other words, something bad happened in the library or
// backend service. Do not confuse with HTTP Internal Server Error; an
// Internal error could also happen on the client code, i.e. when parsing a
// server response.
Internal ErrorCode = "internal"
// Unavailable indicates the service is currently unavailable. This is a most
// likely a transient condition and may be corrected by retrying with a
// backoff.
Unavailable ErrorCode = "unavailable"
// DataLoss indicates unrecoverable data loss or corruption.
DataLoss ErrorCode = "data_loss"
// NoError is the zero-value, is considered an empty error and should not be
// used.
NoError ErrorCode = ""
)
// ServerHTTPStatusFromErrorCode maps a Twirp error type into a similar HTTP
// response status. It is used by the Twirp server handler to set the HTTP
// response status code. Returns 0 if the ErrorCode is invalid.
func ServerHTTPStatusFromErrorCode(code ErrorCode) int {
switch code {
case Canceled:
return 408 // RequestTimeout
case Unknown:
return 500 // Internal Server Error
case InvalidArgument:
return 400 // BadRequest
case DeadlineExceeded:
return 408 // RequestTimeout
case NotFound:
return 404 // Not Found
case BadRoute:
return 404 // Not Found
case AlreadyExists:
return 409 // Conflict
case PermissionDenied:
return 403 // Forbidden
case Unauthenticated:
return 401 // Unauthorized
case ResourceExhausted:
return 403 // Forbidden
case FailedPrecondition:
return 412 // Precondition Failed
case Aborted:
return 409 // Conflict
case OutOfRange:
return 400 // Bad Request
case Unimplemented:
return 501 // Not Implemented
case Internal:
return 500 // Internal Server Error
case Unavailable:
return 503 // Service Unavailable
case DataLoss:
return 500 // Internal Server Error
case NoError:
return 200 // OK
default:
return 0 // Invalid!
}
}
// IsValidErrorCode returns true if is one of the valid predefined constants.
func IsValidErrorCode(code ErrorCode) bool {
return ServerHTTPStatusFromErrorCode(code) != 0
}
// twirp.Error implementation
type twerr struct {
code ErrorCode
msg string
meta map[string]string
}
func (e *twerr) Code() ErrorCode { return e.code }
func (e *twerr) Msg() string { return e.msg }
func (e *twerr) Meta(key string) string {
if e.meta != nil {
return e.meta[key] // also returns "" if key is not in meta map
}
return ""
}
func (e *twerr) WithMeta(key string, value string) Error {
newErr := &twerr{
code: e.code,
msg: e.msg,
meta: make(map[string]string, len(e.meta)),
}
for k, v := range e.meta {
newErr.meta[k] = v
}
newErr.meta[key] = value
return newErr
}
func (e *twerr) MetaMap() map[string]string {
return e.meta
}
func (e *twerr) Error() string {
return fmt.Sprintf("twirp error %s: %s", e.code, e.msg)
}
// wrappedErr fulfills the twirp.Error interface and the
// github.com/pkg/errors.Causer interface. It exposes all the twirp error
// methods, but root cause of an error can be retrieved with
// (*wrappedErr).Cause. This is expected to be used with the InternalErrorWith
// function.
type wrappedErr struct {
wrapper Error
cause error
}
func (e *wrappedErr) Code() ErrorCode { return e.wrapper.Code() }
func (e *wrappedErr) Msg() string { return e.wrapper.Msg() }
func (e *wrappedErr) Meta(key string) string { return e.wrapper.Meta(key) }
func (e *wrappedErr) MetaMap() map[string]string { return e.wrapper.MetaMap() }
func (e *wrappedErr) Error() string { return e.wrapper.Error() }
func (e *wrappedErr) WithMeta(key string, val string) Error {
return &wrappedErr{
wrapper: e.wrapper.WithMeta(key, val),
cause: e.cause,
}
}
func (e *wrappedErr) Cause() error { return e.cause }