diff options
Diffstat (limited to 'vendor/github.com/twitchtv/twirp/errors.go')
| -rw-r--r-- | vendor/github.com/twitchtv/twirp/errors.go | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/vendor/github.com/twitchtv/twirp/errors.go b/vendor/github.com/twitchtv/twirp/errors.go new file mode 100644 index 0000000..b9664b4 --- /dev/null +++ b/vendor/github.com/twitchtv/twirp/errors.go @@ -0,0 +1,428 @@ +// Copyright 2018 Twitch Interactive, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may not +// use this file except in compliance with the License. A copy of the License is +// located at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// or in the "license" file accompanying this file. This file is distributed on +// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +// 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, ok := err.(twirp.Error); ok { +// 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 ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" +) + +// 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 <Code>: <Msg>" + Error() string +} + +// code.Error(msg) builds a new Twirp error with code and msg. Example: +// twirp.NotFound.Error("Resource not found") +// twirp.Internal.Error("Oops") +func (code ErrorCode) Error(msg string) Error { + return NewError(code, msg) +} + +// code.Errorf(msg, args...) builds a new Twirp error with code and formatted msg. +// The format may include "%w" to wrap other errors. Examples: +// twirp.Internal.Error("Oops: %w", originalErr) +// twirp.NotFound.Error("Resource not found with id: %q", resourceID) +func (code ErrorCode) Errorf(msgFmt string, a ...interface{}) Error { + return NewErrorf(code, msgFmt, a...) +} + +// WrapError allows Twirp errors to wrap other errors. +// The wrapped error can be extracted later with (github.com/pkg/errors).Unwrap +// or errors.Is from the standard errors package on Go 1.13+. +func WrapError(twerr Error, err error) Error { + return &wrappedErr{ + wrapper: twerr, + cause: err, + } +} + +// NewError builds a twirp.Error. The code must be one of the valid predefined constants. +// 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: Internal, msg: "invalid error type " + string(code)} + } + return &twerr{code: code, msg: msg} +} + +// NewErrorf builds a twirp.Error with a formatted msg. +// The format may include "%w" to wrap other errors. Examples: +// twirp.NewErrorf(twirp.Internal, "Oops: %w", originalErr) +// twirp.NewErrorf(twirp.NotFound, "resource with id: %q", resourceID) +func NewErrorf(code ErrorCode, msgFmt string, a ...interface{}) Error { + err := fmt.Errorf(msgFmt, a...) // format error message, may include "%w" with an original error + twerr := NewError(code, err.Error()) // use the error as msg + return WrapError(twerr, err) // wrap so the original error can be identified with errors.Is +} + +// NotFoundError is a convenience constructor for NotFound errors. +func NotFoundError(msg string) Error { + return NewError(NotFound, msg) +} + +// InvalidArgumentError is a convenience constructor for InvalidArgument errors. +// The argument name is included on the "argument" metadata for convenience. +func InvalidArgumentError(argument string, validationMsg string) Error { + err := NewError(InvalidArgument, argument+" "+validationMsg) + err = err.WithMeta("argument", argument) + return err +} + +// RequiredArgumentError builds an InvalidArgument error. +// Useful when a request argument is expected to have a non-zero value. +func RequiredArgumentError(argument string) Error { + return InvalidArgumentError(argument, "is required") +} + +// InternalError is a convenience constructor for Internal errors. +func InternalError(msg string) Error { + return NewError(Internal, msg) +} + +// InternalErrorf uses the formatted message as the internal error msg. +// The format may include "%w" to wrap other errors. Examples: +// twirp.InternalErrorf("database error: %w", err) +// twirp.InternalErrorf("failed to load resource %q: %w", resourceID, originalErr) +func InternalErrorf(msgFmt string, a ...interface{}) Error { + return NewErrorf(Internal, msgFmt, a...) +} + +// InternalErrorWith makes an internal error, wrapping the original error and using it +// for the error message, and with metadata "cause" with the original error type. +// This function is used by Twirp services to wrap non-Twirp errors as internal errors. +// The wrapped error can be extracted later with (github.com/pkg/errors).Unwrap +// or errors.Is from the standard errors package on Go 1.13+. +func InternalErrorWith(err error) Error { + twerr := NewError(Internal, err.Error()) + twerr = twerr.WithMeta("cause", fmt.Sprintf("%T", err)) // to easily tell apart wrapped internal errors from explicit ones + return WrapError(twerr, 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" + + // Malformed indicates an error occurred while decoding the client's request. + // This may mean that the message was encoded improperly, or that there is a + // disagreement in message format between the client and server. + Malformed ErrorCode = "malformed" + + // 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 or rate-limited, + // 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 Malformed: + 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 429 // Too Many Requests + 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 is the error returned by twirp.InternalErrorWith(err), which is used by clients. +// Implements Unwrap() to allow go 1.13+ errors.Is/As checks, +// and Cause() to allow (github.com/pkg/errors).Unwrap. +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) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As +func (e *wrappedErr) Cause() error { return e.cause } // for github.com/pkg/errors + +// WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). +// Useful outside of the Twirp server (e.g. http middleware). +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func WriteError(resp http.ResponseWriter, err error) error { + var twerr Error + if !errors.As(err, &twerr) { + twerr = InternalErrorWith(err) + } + + statusCode := ServerHTTPStatusFromErrorCode(twerr.Code()) + respBody := marshalErrorToJSON(twerr) + + resp.Header().Set("Content-Type", "application/json") // Error responses are always JSON + resp.Header().Set("Content-Length", strconv.Itoa(len(respBody))) + resp.WriteHeader(statusCode) // set HTTP status code and send response + + _, writeErr := resp.Write(respBody) + if writeErr != nil { + return writeErr + } + return nil +} + +// JSON serialization for errors +type twerrJSON struct { + Code string `json:"code"` + Msg string `json:"msg"` + Meta map[string]string `json:"meta,omitempty"` +} + +// marshalErrorToJSON returns JSON from a twirp.Error, that can be used as HTTP error response body. +// If serialization fails, it will use a descriptive Internal error instead. +func marshalErrorToJSON(twerr Error) []byte { + // make sure that msg is not too large + msg := twerr.Msg() + if len(msg) > 1e6 { + msg = msg[:1e6] + } + + tj := twerrJSON{ + Code: string(twerr.Code()), + Msg: msg, + Meta: twerr.MetaMap(), + } + + buf, err := json.Marshal(&tj) + if err != nil { + buf = []byte("{\"type\": \"" + Internal + "\", \"msg\": \"There was an error but it could not be serialized into JSON\"}") // fallback + } + + return buf +} |
