481 lines
11 KiB
Go
481 lines
11 KiB
Go
// Package errors provides a way to return detailed information
|
|
// for an RPC request error. The error is normally JSON encoded.
|
|
package errors // import "go.unistack.org/micro/v3/errors"
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
var (
|
|
// ErrBadRequest returns then requests contains invalid data
|
|
ErrBadRequest = &Error{Code: 400}
|
|
// ErrUnauthorized returns then user have unauthorized call
|
|
ErrUnauthorized = &Error{Code: 401}
|
|
// ErrForbidden returns then user have not access the resource
|
|
ErrForbidden = &Error{Code: 403}
|
|
// ErrNotFound returns then user specify invalid endpoint
|
|
ErrNotFound = &Error{Code: 404}
|
|
// ErrMethodNotAllowed returns then user try to get invalid method
|
|
ErrMethodNotAllowed = &Error{Code: 405}
|
|
// ErrTimeout returns then timeout exceeded
|
|
ErrTimeout = &Error{Code: 408}
|
|
// ErrConflict returns then request create duplicate resource
|
|
ErrConflict = &Error{Code: 409}
|
|
// ErrInternalServerError returns then server cant process request because of internal error
|
|
ErrInternalServerError = &Error{Code: 500}
|
|
// ErNotImplemented returns then server does not have desired endpoint method
|
|
ErNotImplemented = &Error{Code: 501}
|
|
// ErrBadGateway returns then server cant process request
|
|
ErrBadGateway = &Error{Code: 502}
|
|
// ErrServiceUnavailable returns then service unavailable
|
|
ErrServiceUnavailable = &Error{Code: 503}
|
|
// ErrGatewayTimeout returns then server have long time to process request
|
|
ErrGatewayTimeout = &Error{Code: 504}
|
|
)
|
|
|
|
// Error type
|
|
type Error struct {
|
|
// ID holds error id or service, usually someting like my_service or id
|
|
ID string
|
|
// Detail holds some useful details about error
|
|
Detail string
|
|
// Status usually holds text of http status
|
|
Status string
|
|
// Code holds error code
|
|
Code int32
|
|
}
|
|
|
|
// Error satisfies error interface
|
|
func (e *Error) Error() string {
|
|
b, _ := json.Marshal(e)
|
|
return string(b)
|
|
}
|
|
|
|
/*
|
|
// Generator struct holds id of error
|
|
type Generator struct {
|
|
id string
|
|
}
|
|
|
|
// Generator can emit new error with static id
|
|
func NewGenerator(id string) *Generator {
|
|
return &Generator{id: id}
|
|
}
|
|
|
|
func (g *Generator) BadRequest(format string, args ...interface{}) error {
|
|
return BadRequest(g.id, format, args...)
|
|
}
|
|
*/
|
|
|
|
// New generates a custom error
|
|
func New(id, detail string, code int32) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: code,
|
|
Detail: detail,
|
|
Status: http.StatusText(int(code)),
|
|
}
|
|
}
|
|
|
|
// Parse tries to parse a JSON string into an error. If that
|
|
// fails, it will set the given string as the error detail.
|
|
func Parse(err string) *Error {
|
|
e := &Error{}
|
|
nerr := json.Unmarshal([]byte(err), e)
|
|
if nerr != nil {
|
|
e.Detail = err
|
|
}
|
|
return e
|
|
}
|
|
|
|
// BadRequest generates a 400 error.
|
|
func BadRequest(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 400,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(400),
|
|
}
|
|
}
|
|
|
|
// Unauthorized generates a 401 error.
|
|
func Unauthorized(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 401,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(401),
|
|
}
|
|
}
|
|
|
|
// Forbidden generates a 403 error.
|
|
func Forbidden(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 403,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(403),
|
|
}
|
|
}
|
|
|
|
// NotFound generates a 404 error.
|
|
func NotFound(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 404,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(404),
|
|
}
|
|
}
|
|
|
|
// MethodNotAllowed generates a 405 error.
|
|
func MethodNotAllowed(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 405,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(405),
|
|
}
|
|
}
|
|
|
|
// Timeout generates a 408 error.
|
|
func Timeout(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 408,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(408),
|
|
}
|
|
}
|
|
|
|
// Conflict generates a 409 error.
|
|
func Conflict(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 409,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(409),
|
|
}
|
|
}
|
|
|
|
// InternalServerError generates a 500 error.
|
|
func InternalServerError(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 500,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(500),
|
|
}
|
|
}
|
|
|
|
// NotImplemented generates a 501 error
|
|
func NotImplemented(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 501,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(501),
|
|
}
|
|
}
|
|
|
|
// BadGateway generates a 502 error
|
|
func BadGateway(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 502,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(502),
|
|
}
|
|
}
|
|
|
|
// ServiceUnavailable generates a 503 error
|
|
func ServiceUnavailable(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 503,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(503),
|
|
}
|
|
}
|
|
|
|
// GatewayTimeout generates a 504 error
|
|
func GatewayTimeout(id, format string, args ...interface{}) error {
|
|
return &Error{
|
|
ID: id,
|
|
Code: 504,
|
|
Detail: fmt.Sprintf(format, args...),
|
|
Status: http.StatusText(504),
|
|
}
|
|
}
|
|
|
|
// Equal tries to compare errors
|
|
func Equal(err1 error, err2 error) bool {
|
|
verr1, ok1 := err1.(*Error)
|
|
verr2, ok2 := err2.(*Error)
|
|
|
|
if ok1 != ok2 {
|
|
return false
|
|
}
|
|
|
|
if !ok1 {
|
|
return err1 == err2
|
|
}
|
|
|
|
if verr1.Code != verr2.Code {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// CodeIn return true if err has specified code
|
|
func CodeIn(err interface{}, codes ...int32) bool {
|
|
var code int32
|
|
switch verr := err.(type) {
|
|
case *Error:
|
|
code = verr.Code
|
|
case int32:
|
|
code = verr
|
|
default:
|
|
return false
|
|
}
|
|
|
|
for _, check := range codes {
|
|
if code == check {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// FromError try to convert go error to *Error
|
|
func FromError(err error) *Error {
|
|
if verr, ok := err.(*Error); ok && verr != nil {
|
|
return verr
|
|
}
|
|
|
|
return Parse(err.Error())
|
|
}
|
|
|
|
// MarshalJSON returns error data
|
|
func (e *Error) MarshalJSON() ([]byte, error) {
|
|
return e.Marshal()
|
|
}
|
|
|
|
// UnmarshalJSON set error data
|
|
func (e *Error) UnmarshalJSON(data []byte) error {
|
|
return e.Unmarshal(data)
|
|
}
|
|
|
|
// ProtoMessage noop func
|
|
func (e *Error) ProtoMessage() {}
|
|
|
|
// Reset resets error
|
|
func (e *Error) Reset() {
|
|
*e = Error{}
|
|
}
|
|
|
|
// String returns error as string
|
|
func (e *Error) String() string {
|
|
return fmt.Sprintf(`{"id":"%s","detail":"%s","status":"%s","code":%d}`, addslashes(e.ID), addslashes(e.Detail), addslashes(e.Status), e.Code)
|
|
}
|
|
|
|
// Marshal returns error data
|
|
func (e *Error) Marshal() ([]byte, error) {
|
|
return []byte(e.String()), nil
|
|
}
|
|
|
|
// Unmarshal set error data
|
|
func (e *Error) Unmarshal(data []byte) error {
|
|
str := string(data)
|
|
if len(data) < 41 {
|
|
return fmt.Errorf("invalid data")
|
|
}
|
|
parts := strings.FieldsFunc(str[1:len(str)-1], func(r rune) bool {
|
|
return r == ','
|
|
})
|
|
for _, part := range parts {
|
|
nparts := strings.FieldsFunc(part, func(r rune) bool {
|
|
return r == ':'
|
|
})
|
|
for idx := 0; idx < len(nparts)/2; idx += 2 {
|
|
val := strings.Trim(nparts[idx+1], `"`)
|
|
if len(val) == 0 {
|
|
continue
|
|
}
|
|
switch {
|
|
case nparts[idx] == `"id"`:
|
|
e.ID = val
|
|
case nparts[idx] == `"detail"`:
|
|
e.Detail = val
|
|
case nparts[idx] == `"status"`:
|
|
e.Status = val
|
|
case nparts[idx] == `"code"`:
|
|
c, err := strconv.ParseInt(val, 10, 32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Code = int32(c)
|
|
}
|
|
idx++
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func addslashes(str string) string {
|
|
var buf bytes.Buffer
|
|
for _, char := range str {
|
|
switch char {
|
|
case '\'', '"', '\\':
|
|
buf.WriteRune('\\')
|
|
}
|
|
buf.WriteRune(char)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
type retryableError struct {
|
|
err error
|
|
}
|
|
|
|
// Retryable returns error that can be retried later
|
|
func Retryable(err error) error {
|
|
return &retryableError{err: err}
|
|
}
|
|
|
|
type IsRetryableFunc func(error) bool
|
|
|
|
var (
|
|
RetrayableOracleErrors = []IsRetryableFunc{
|
|
func(err error) bool {
|
|
errmsg := err.Error()
|
|
switch {
|
|
case strings.Contains(errmsg, `ORA-`):
|
|
return true
|
|
case strings.Contains(errmsg, `can not assign`):
|
|
return true
|
|
case strings.Contains(errmsg, `can't assign`):
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
}
|
|
RetrayablePostgresErrors = []IsRetryableFunc{
|
|
func(err error) bool {
|
|
errmsg := err.Error()
|
|
switch {
|
|
case strings.Contains(errmsg, `number of field descriptions must equal number of`):
|
|
return true
|
|
case strings.Contains(errmsg, `not a pointer`):
|
|
return true
|
|
case strings.Contains(errmsg, `values, but dst struct has only`):
|
|
return true
|
|
case strings.Contains(errmsg, `struct doesn't have corresponding row field`):
|
|
return true
|
|
case strings.Contains(errmsg, `cannot find field`):
|
|
return true
|
|
case strings.Contains(errmsg, `cannot scan`) || strings.Contains(errmsg, `cannot convert`):
|
|
return true
|
|
case strings.Contains(errmsg, `failed to connect to`):
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
}
|
|
RetryableMicroErrors = []IsRetryableFunc{
|
|
func(err error) bool {
|
|
switch verr := err.(type) {
|
|
case *Error:
|
|
switch verr.Code {
|
|
case 401, 403, 408, 500, 501, 502, 503, 504:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case *retryableError:
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
}
|
|
RetryableGoErrors = []IsRetryableFunc{
|
|
func(err error) bool {
|
|
switch verr := err.(type) {
|
|
case interface{ SafeToRetry() bool }:
|
|
return verr.SafeToRetry()
|
|
case interface{ Timeout() bool }:
|
|
return verr.Timeout()
|
|
}
|
|
switch {
|
|
case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF):
|
|
return true
|
|
case errors.Is(err, context.DeadlineExceeded):
|
|
return true
|
|
case errors.Is(err, io.ErrClosedPipe), errors.Is(err, io.ErrShortBuffer), errors.Is(err, io.ErrShortWrite):
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
}
|
|
RetryableGrpcErrors = []IsRetryableFunc{
|
|
func(err error) bool {
|
|
st, ok := status.FromError(err)
|
|
if !ok {
|
|
return false
|
|
}
|
|
switch st.Code() {
|
|
case codes.Unavailable, codes.ResourceExhausted:
|
|
return true
|
|
case codes.DeadlineExceeded:
|
|
return true
|
|
case codes.Internal:
|
|
switch {
|
|
case strings.Contains(st.Message(), `transport: received the unexpected content-type "text/html; charset=UTF-8"`):
|
|
return true
|
|
case strings.Contains(st.Message(), io.ErrUnexpectedEOF.Error()):
|
|
return true
|
|
case strings.Contains(st.Message(), `stream terminated by RST_STREAM with error code: INTERNAL_ERROR`):
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
}
|
|
)
|
|
|
|
// Unwrap provides error wrapping
|
|
func (e *retryableError) Unwrap() error {
|
|
return e.err
|
|
}
|
|
|
|
// Error returns the error string
|
|
func (e *retryableError) Error() string {
|
|
if e.err == nil {
|
|
return ""
|
|
}
|
|
return e.err.Error()
|
|
}
|
|
|
|
// IsRetryable checks error for ability to retry later
|
|
func IsRetryable(err error, fns ...IsRetryableFunc) bool {
|
|
for _, fn := range fns {
|
|
if ok := fn(err); ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|