Skip to content
Snippets Groups Projects
Select Git revision
  • 16c5af8e169e43e995d901c8356de3b2d489c263
  • main default protected
  • trading_hours
  • refactor_trading_hours
  • audit_cleaning_cater_for_non_struct_fields
  • remove-info-logs
  • sl-refactor
  • 18-use-scan-for-param-values
  • 17-order-search-results
  • 4-simplify-framework-2
  • 1-http-error
  • v1.297.0
  • v1.296.0
  • v1.295.0
  • v1.294.0
  • v1.293.0
  • v1.292.0
  • v1.291.0
  • v1.290.0
  • v1.289.0
  • v1.288.0
  • v1.287.0
  • v1.286.0
  • v1.285.0
  • v1.284.0
  • v1.283.0
  • v1.282.0
  • v1.281.0
  • v1.280.0
  • v1.279.0
  • v1.278.0
31 results

error.go

Blame
  • 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
    }