Select Git revision
-
Jano Hendriks authoredJano Hendriks authored
error.go 7.25 KiB
package errors
import (
"fmt"
"net/http"
"path"
"strconv"
"strings"
)
// CustomError implements the following interfaces:
//
// error
// github.com/pkg/errors: Cause
type CustomError struct {
code int
message string
caller Caller
cause error
bypassRaygun bool
bypassSQSError bool
}
// implement interface error:
func (err *CustomError) Error() string {
return err.Formatted(FormattingOptions{Causes: false})
}
func (err *CustomError) BypassRaygun() *CustomError {
err.bypassRaygun = true
return err
}
func (err *CustomError) ShouldBypassRaygun() bool {
return err.bypassRaygun
}
func (err *CustomError) BypassSQSError() *CustomError {
err.bypassSQSError = true
return err
}
func (err *CustomError) ShouldBypassSQSError() bool {
return err.bypassSQSError
}
func Is(e1, e2 error) bool {
if e1WithIs, ok := e1.(ErrorWithIs); ok {
return e1WithIs.Is(e2)
}
return e1.Error() == e2.Error()
}
// Is() compares the message string of this or any cause to match the specified error message
func (err *CustomError) Is(specificError error) bool {
if err.message == specificError.Error() {
return true
}
if err.cause != nil {
if causeWithIs, ok := err.cause.(ErrorWithIs); ok {
return causeWithIs.Is(specificError)
}
}
return false
}
// implement github.com/pkg/errors: Cause
func (err *CustomError) Cause() error {
return err.cause
}
func HTTPCode(err error) int {
if errWithCode, ok := err.(ErrorWithCause); ok {
return errWithCode.Code()
}
return 0
}
func (err *CustomError) Code() int {
// find http error code - returning the smallest code in the stack of causes (excluding code==0)
code := err.code
if err.cause != nil {
if causeWithCode, ok := err.cause.(ErrorWithCause); ok {
causeCode := causeWithCode.Code()
if code == 0 || (causeCode != 0 && causeCode < code) {
code = causeCode
}
}
}
return code
}
func (err *CustomError) Description() Description {
info := err.caller.Info()
desc := &Description{
Message: err.message,
Source: &info,
}
if err.cause != nil {
causeWithStack, ok := err.cause.(*CustomError)
if !ok {
// external cause without our stack
// if github.com/pkg/errors, we can still get caller reference
desc.Cause = pkgDescription(0, err.cause)
} else {
// cause has our stack
causeDesription := causeWithStack.Description()
desc.Cause = &causeDesription
}
}
return *desc
}
func (err *CustomError) Format(s fmt.State, v rune) {
s.Write([]byte(
err.Formatted(
FormattingOptions{
Causes: (v == 'v' || v == 'c'), // include causes for %c and %v, s is only this error
NewLines: v == 'v', // use newlines only on v, c is more compact on single line
Source: s.Flag('+'), // include source references when %+v or %+c
},
),
))
}
type FormattingOptions struct {
Causes bool
NewLines bool
Source bool
}
func (err *CustomError) Formatted(opts FormattingOptions) string {
// start with this error
thisError := ""
if opts.Source {
thisError += fmt.Sprintf("%s/%s(%d): %s() ",
err.caller.Package(),
path.Base(err.caller.File()),
err.caller.Line(),
err.caller.Function(),
)
}
if err.cause == nil || !opts.Causes {
return err.message
}
if err.cause.Error() != err.message {
thisError += err.message
}
sep := ""
if len(thisError) > 0 {
sep = ", because"
if opts.NewLines {
sep += "\n\t"
} else {
sep += " "
}
}
if causeWithStack, ok := err.cause.(*CustomError); ok {
return thisError + sep + causeWithStack.Formatted(opts)
}
// this level does not have our own stack, but we can detect github.com/pkg/errors stack, then:
// note: do not use fmt.Sprintf("%+v", err.cause) because it will repeat the stack if multiple were captured
// note: do not use err.cause.Error() because it does not include any stack
// instead, get first layer that implements it and log it
return thisError + pkgStack(err.cause, opts)
}
func pkgStack(err error, opts FormattingOptions) string {
s := ""
e := err
for e != nil {
if errWithStackTracer, ok := e.(stackTracer); ok {
st := errWithStackTracer.StackTrace()
for _, f := range st {
// source := fmt.Sprintf("%n %s %d", f, f, f) - this shows only package name, not fully qualified package name :-(
sources := strings.SplitN(fmt.Sprintf("%+s(%d)\n%n", f, f, f), "\n", 3)
// package <-- sources[0]
// full filename:line <-- source[1]
// function name <-- source[2]
// skip runtime packages
if strings.HasPrefix(sources[0], "runtime") {
break
}
if opts.NewLines {
s += ", because \n\t"
} else {
s += ", because "
}
if opts.Source {
s += sources[0] + "/" + path.Base(sources[1]) + ": " + sources[2] + "(): "
}
s += fmt.Sprintf("%s", e)
}
return s
} else {
// no stack tracer...
if opts.NewLines {
s += ", because \n\t"
} else {
s += ", because "
}
s += e.Error()
}
errWithCause, ok := e.(ErrorWithCause)
if !ok {
break
}
e = errWithCause.Cause()
}
return s
}
func pkgDescription(level int, err error) *Description {
desc := &Description{
Message: fmt.Sprintf("%s", err),
Source: nil,
Cause: nil,
}
// recursively fill causes first
if errWithCause, ok := err.(ErrorWithCause); ok {
causeErr := errWithCause.Cause()
if causeErr != nil {
desc.Cause = pkgDescription(level+1, causeErr)
}
}
if errWithStackTracer, ok := err.(stackTracer); ok {
tempDesc := desc
st := errWithStackTracer.StackTrace()
for _, f := range st {
if tempDesc == nil {
break // stop if no more causes populated
}
// source := fmt.Sprintf("%n %s %d", f, f, f) - this shows only package name, not fully qualified package name :-(
sources := strings.SplitN(fmt.Sprintf("%+s\n%d\n%n", f, f, f), "\n", 4)
// package <-- sources[0]
// full filename <-- source[1]
//line <-- source[2]
// function name <-- source[3]
// stop if entering runtime part of the stack
if strings.HasPrefix(sources[0], "runtime") {
break
}
tempDesc.Source = &CallerInfo{
Package: sources[0],
File: path.Base(sources[1]),
// Line: sources[2],
Function: sources[3],
}
if i64, err := strconv.ParseInt(sources[2], 10, 64); err == nil {
tempDesc.Source.Line = int(i64)
}
tempDesc = tempDesc.Cause
} // for each stack level
} // if has stack
return desc
}
func IsRetryableError(err error) bool {
code := HTTPCode(err)
if code == 0 {
return false
}
// 429 should always retry
if code == http.StatusTooManyRequests {
return true
}
// Retry for all except 200s and 400s
if code < 200 || (code >= 300 && code < 400) || code >= 500 {
return true
}
return false
}
func IsRetryableErrorOrShouldFail(err error) (shouldRetry bool, shouldFail bool) {
if err == nil {
return false, false
}
// Check for retryable
if IsRetryableError(err) {
return true, false
}
code := HTTPCode(err)
// If no HTTP code in the error, just fail but don't retry
if code == 0 {
return false, true
}
// Explicitly check for 400s, and don't retry or fail
if code >= 400 && code < 500 {
return false, false
}
// Explicitly check for 200s, for a fail-safe
if code >= 200 && code < 300 {
return false, false
}
// If we got here, we should fail normally
return false, true
}