From 15053c0159679c87b660b20cd6ee2b9a64ba1766 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Tue, 7 May 2024 21:58:04 +0530 Subject: [PATCH 01/14] feat: introduce errkit --- errkit/README.md | 58 +++++++++++ errkit/attr.go | 19 ++++ errkit/errors.go | 223 ++++++++++++++++++++++++++++++++++++++++++ errkit/errors_test.go | 80 +++++++++++++++ errkit/helpers.go | 129 ++++++++++++++++++++++++ errkit/interfaces.go | 32 ++++++ errkit/kind.go | 187 +++++++++++++++++++++++++++++++++++ 7 files changed, 728 insertions(+) create mode 100644 errkit/README.md create mode 100644 errkit/attr.go create mode 100644 errkit/errors.go create mode 100644 errkit/errors_test.go create mode 100644 errkit/helpers.go create mode 100644 errkit/interfaces.go create mode 100644 errkit/kind.go diff --git a/errkit/README.md b/errkit/README.md new file mode 100644 index 0000000..279a163 --- /dev/null +++ b/errkit/README.md @@ -0,0 +1,58 @@ +# errkit + +why errkit when we already have errorutil ? + +---- + +errorutil was introduced a year ago with main goal to capture stack of error to identify underlying deeply nested errors. but it does not follow the go paradigm of error handling and implementation and is counter intuitive to that. i.e in golang looking at any error util/implementation library "errors" "pkg/errors" or "uber.go/multierr" etc, they all follow the same pattern i.e `.Error()` method is never used and instead it is wrapped with helper structs following a particular interface which allows traversing the error chain and using helper functions like `.Cause() error` or `.Unwrap() error` or `errors.Is()` and more. but errorutil marshalls the error to string which does not play well with the go error handling paradigm. Apart from that over time usage of errorutil has been cumbersome because it is not drop in replacement for any error package and it does not allow propogating/traversing error chain in a go idiomatic way. + + +`errkit` is new error library that is built upon learnings from `errorutil` and has following features: + +- drop in replacement for (no syntax change / refactor required) + - `errors` package + - `pkg/errors` package (now deprecated) + - `uber/multierr` package +- is compatible with all known go error handling implementations and can parse errors from any library and is compatible with existing error handling libraries and helper functions like `Is()` , `As()` , `Cause()` and more. +- is go idiomatic and follows the go error handling paradigm +- Has Attributes support (see below) +- Implements and categorizes errors into different classes (see below) + - `ErrClassNetworkTemporary` + - `ErrClassNetworkPermanent` + - `ErrClassDeadline` + - Custom Classes via `ErrClass` interface +- Supports easy conversion to slog Item for structured logging reatining all error info +- Helper functions to implement public/user facing errors by using error classes + + +**Attributes Support** + +To strictly follow the go error handling paradigm and making it easy to traverse error chain, errkit support adding `Attr(key comparable,value any)` to error instead of wrapping it with a string message. This keeps extra error info minimal without being too verbose a good example of this is following + +```go +// normal way of error propogating through nested stack +err := errkit.New("i/o timeout") + +// xyz.go +err := errkit.Wrap(err,"failed to connect %s",addr) + +// abc.go +err := errkit.Wrap(err,"error occured when downloading %s",xyz) +``` + +with attributes support you can do following + +```go +// normal way of error propogating through nested stack +err := errkit.New("i/o timeout") + +// xyz.go +err = errkit.WithAttr(err,&errkit.Resource{},errkit.Resource{type: "network",add: addr}) + +// abc.go +err = errkit.WithAttr(err,&errkit.Action{},errkit.Action{type: "download"}) +``` + +the good part is that all attributes must implement a interface 'ErrAttr' that way all of them are compatible and can be consolidated into a struct/object of choice. for more example see `attr_test.go` + +In case the same attribute types are added they are deduplicated / consolidated into parent attribute if it supports that. \ No newline at end of file diff --git a/errkit/attr.go b/errkit/attr.go new file mode 100644 index 0000000..602c9bc --- /dev/null +++ b/errkit/attr.go @@ -0,0 +1,19 @@ +package errkit + +// AttrResource is a resource attribute +// which can be used to represent a resource +type AttrResource struct { + // Type is the type of resource + Type string + // Value is the value of resource + Value string +} + +// AttrAction is an action attribute +// which can be used to represent an action +type AttrAction struct { + // Type is the type of action + Type string + // Value is the value of action + Value string +} diff --git a/errkit/errors.go b/errkit/errors.go new file mode 100644 index 0000000..729b2f1 --- /dev/null +++ b/errkit/errors.go @@ -0,0 +1,223 @@ +// errkit implements all errors generated by nuclei and includes error definations +// specific to nuclei , error classification (like network,logic) etc +package errkit + +import ( + "encoding/json" + "errors" + "fmt" + "log/slog" + "strings" + + "github.com/projectdiscovery/utils/env" +) + +const ( + // DelimArrow is delim used by projectdiscovery/utils to join errors + DelimArrow = "<-" + // DelimArrowSerialized + DelimArrowSerialized = "\u003c-" + // DelimSemiColon is standard delim popularly used to join errors + DelimSemiColon = "; " + // DelimMultiLine is delim used to join errors in multiline format + DelimMultiLine = "\n - " + // MultiLinePrefix is the prefix used for multiline errors + MultiLineErrPrefix = "the following errors occurred:" +) + +var ( + // MaxErrorDepth is the maximum depth of errors to be unwrapped or maintained + // all errors beyond this depth will be ignored + MaxErrorDepth = env.GetEnvOrDefault("MAX_ERROR_DEPTH", 3) + // ErrorSeperator is the seperator used to join errors + ErrorSeperator = env.GetEnvOrDefault("ERROR_SEPERATOR", "; ") +) + +// ErrorX is a custom error type that can handle all known types of errors +// wrapping and joining strategies including custom ones and it supports error class +// which can be shown to client/users in more meaningful way +type ErrorX struct { + kind ErrKind + attrs []slog.Attr + errs []error +} + +// Build returns the object as error interface +func (e *ErrorX) Build() error { + return e +} + +// Unwrap returns the underlying error +func (e *ErrorX) Unwrap() []error { + return e.errs +} + +// Is checks if current error contains given error +func (e *ErrorX) Is(err error) bool { + x := &ErrorX{} + parseError(x, err) + // even one submatch is enough + for _, orig := range e.errs { + for _, match := range x.errs { + if errors.Is(orig, match) { + return true + } + } + } + + return false +} + +// MarshalJSON returns the json representation of the error +func (e *ErrorX) MarshalJSON() ([]byte, error) { + m := map[string]interface{}{ + "kind": e.kind.String(), + "errors": e.errs, + } + return json.Marshal(m) +} + +// Error returns the error string +func (e *ErrorX) Error() string { + var sb strings.Builder + if e.kind != nil { + sb.WriteString("errKind=") + sb.WriteString(e.kind.String()) + sb.WriteString(" ") + } + for _, err := range e.errs { + sb.WriteString(err.Error()) + sb.WriteString(ErrorSeperator) + } + return strings.TrimSuffix(sb.String(), ErrorSeperator) +} + +// Cause return the original error that caused this without any wrapping +func (e *ErrorX) Cause() error { + if len(e.errs) > 0 { + return e.errs[0] + } + return nil +} + +// Kind returns the errorkind associated with this error +// if any +func (e *ErrorX) Kind() ErrKind { + return e.kind +} + +// FromError parses a given error to understand the error class +// and optionally adds given message for more info +func FromError(err error) *ErrorX { + if err == nil { + return nil + } + nucleiErr := &ErrorX{} + parseError(nucleiErr, err) + return nucleiErr +} + +// New creates a new error with the given message +func New(format string, args ...interface{}) *ErrorX { + return &ErrorX{errs: []error{fmt.Errorf(format, args...)}} +} + +// Msgf adds a message to the error +func (e *ErrorX) Msgf(format string, args ...interface{}) { + if e == nil { + return + } + e.errs = append(e.errs, fmt.Errorf(format, args...)) +} + +// SetClass sets the class of the error +// if underlying error class was already set, then it is given preference +// when generating final error msg +func (e *ErrorX) SetKind(kind ErrKind) *ErrorX { + if e.kind == nil { + e.kind = kind + } else { + e.kind = CombineErrKinds(e.kind, kind) + } + return e +} + +// SetAttr sets additional attributes to a given error +// Note: key must be comparable and key and value both cannot +// be nil similar to context.WithValue +func (e *ErrorX) SetAttr(s slog.Attr) *ErrorX { + if len(e.attrs) >= MaxErrorDepth { + return e + } + e.attrs = append(e.attrs, s) + return e +} + +// parseError recursively parses all known types of errors +func parseError(to *ErrorX, err error) { + if err == nil { + return + } + if to == nil { + to = &ErrorX{} + } + if len(to.errs) >= MaxErrorDepth { + return + } + + switch v := err.(type) { + case *ErrorX: + to.errs = append(to.errs, v.errs...) + to.kind = CombineErrKinds(to.kind, v.kind) + case JoinedError: + to.errs = append(to.errs, v.Unwrap()...) + case WrappedError: + to.errs = append(to.errs, v.Unwrap()) + case CauseError: + to.errs = append(to.errs, v.Cause()) + remaining := strings.Replace(err.Error(), v.Cause().Error(), "", -1) + parseError(to, errors.New(remaining)) + default: + errString := err.Error() + // try assigning to enriched error + if strings.Contains(errString, DelimArrow) { + // Split the error by arrow delim + parts := strings.Split(errString, DelimArrow) + for i := len(parts) - 1; i >= 0; i-- { + part := strings.TrimSpace(parts[i]) + parseError(to, errors.New(part)) + } + } else if strings.Contains(errString, DelimArrowSerialized) { + // Split the error by arrow delim + parts := strings.Split(errString, DelimArrowSerialized) + for i := len(parts) - 1; i >= 0; i-- { + part := strings.TrimSpace(parts[i]) + parseError(to, errors.New(part)) + } + } else if strings.Contains(errString, DelimSemiColon) { + // Split the error by semi-colon delim + parts := strings.Split(errString, DelimSemiColon) + for _, part := range parts { + part = strings.TrimSpace(part) + parseError(to, errors.New(part)) + } + } else if strings.Contains(errString, MultiLineErrPrefix) { + // remove prefix + msg := strings.ReplaceAll(errString, MultiLineErrPrefix, "") + parts := strings.Split(msg, DelimMultiLine) + for _, part := range parts { + part = strings.TrimSpace(part) + parseError(to, errors.New(part)) + } + } else { + // this cannot be furthur unwrapped + to.errs = append(to.errs, err) + } + } +} + +// WrappedError is implemented by errors that are wrapped +type WrappedError interface { + // Unwrap returns the underlying error + Unwrap() error +} diff --git a/errkit/errors_test.go b/errkit/errors_test.go new file mode 100644 index 0000000..824e699 --- /dev/null +++ b/errkit/errors_test.go @@ -0,0 +1,80 @@ +package errkit + +import ( + "testing" + + "github.com/pkg/errors" + errorutil "github.com/projectdiscovery/utils/errors" + "go.uber.org/multierr" + + stderrors "errors" +) + +// what are these tests ? +// Below tests check for interoperability of this package with other error packages +// like pkg/errors and go.uber.org/multierr and std errors as well + +func TestErrorAs(t *testing.T) { + // Create a new error with a specific class and wrap it + x := New("this is a nuclei error").SetKind(ErrClassNetworkPermanent).Build() + y := errors.Wrap(x, "this is a wrap error") + + // Attempt to unwrap the error to a specific type + ne := &ErrorX{} + if !errors.As(y, &ne) { + t.Fatal("expected to be able to unwrap") + } + + // Wrap the specific error type into another error and try unwrapping again + wrapped := Wrap(ne, "this is a wrapped error") + if !errors.As(wrapped, &ne) { + t.Fatal("expected to be able to unwrap") + } + + // Combine multiple errors into a multierror and attempt to unwrap to the specific type + errs := []error{ + stderrors.New("this is a std error"), + x, + errors.New("this is a pkg error"), + } + multi := multierr.Combine(errs...) + if !errors.As(multi, &ne) { + t.Fatal("expected to be able to unwrap") + } +} + +func TestErrorIs(t *testing.T) { + // Create a new error, wrap it, and check if the original error can be found + x := New("this is a nuclei error").SetKind(ErrClassNetworkPermanent).Build() + y := errors.Wrap(x, "this is a wrap error") + if !errors.Is(y, x) { + t.Fatal("expected to be able to find the original error") + } + + // Wrap the original error with a custom wrapper and check again + wrapped := Wrap(x, "this is a wrapped error") + if !stderrors.Is(wrapped, x) { + t.Fatal("expected to be able to find the original error") + } + + // Combine multiple errors into a multierror and check if the original error can be found + errs := []error{ + stderrors.New("this is a std error"), + x, + errors.New("this is a pkg error"), + } + multi := multierr.Combine(errs...) + if !errors.Is(multi, x) { + t.Fatal("expected to be able to find the original error") + } +} + +func TestErrorUtil(t *testing.T) { + utilErr := errorutil.New("got err while executing http://206.189.19.240:8000/wp-content/plugins/wp-automatic/inc/csv.php <- POST http://206.189.19.240:8000/wp-content/plugins/wp-automatic/inc/csv.php giving up after 2 attempts: Post \"http://206.189.19.240:8000/wp-content/plugins/wp-automatic/inc/csv.php\": [:RUNTIME] ztls fallback failed <- dial tcp 206.189.19.240:8000: connect: connection refused") + x := ErrorX{} + parseError(&x, utilErr) + if len(x.errs) != 3 { + t.Fatal("expected 3 errors") + } + t.Log(x.errs) +} diff --git a/errkit/helpers.go b/errkit/helpers.go new file mode 100644 index 0000000..4150fb6 --- /dev/null +++ b/errkit/helpers.go @@ -0,0 +1,129 @@ +package errkit + +import "errors" + +// Proxy to StdLib errors.Is +func Is(err error, target ...error) bool { + if err == nil { + return false + } + for _, t := range target { + if t == nil { + continue + } + if errors.Is(err, t) { + return true + } + } + return false +} + +// Proxy to StdLib errors.As +func As(err error, target interface{}) bool { + return errors.As(err, target) +} + +// Combine combines multiple errors into a single error +func Combine(errs ...error) error { + if len(errs) == 0 { + return nil + } + x := &ErrorX{} + for _, err := range errs { + if err == nil { + continue + } + parseError(x, err) + } + return x +} + +// Wrap wraps the given error with the message +func Wrap(err error, message string) error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + x.Msgf(message) + return x +} + +// Wrapf wraps the given error with the message +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + x.Msgf(format, args...) + return x +} + +// Errors returns all underlying errors there were appended or joined +func Errors(err error) []error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + return x.errs +} + +// Append appends given errors and returns a new error +// it ignores all nil errors +func Append(errs ...error) error { + if len(errs) == 0 { + return nil + } + x := &ErrorX{} + for _, err := range errs { + if err == nil { + continue + } + parseError(x, err) + } + return x +} + +// Cause returns the original error that caused this error +func Cause(err error) error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + return x.Cause() +} + +// WithMessage +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + x.Msgf(message) + return x +} + +// WithMessagef +func WithMessagef(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + x.Msgf(format, args...) + return x +} + +// IsNetworkTemporaryErr checks if given error is a temporary network error +func IsNetworkTemporaryErr(err error) bool { + if err == nil { + return false + } + x := &ErrorX{} + parseError(x, err) + return isNetworkTemporaryErr(x) +} diff --git a/errkit/interfaces.go b/errkit/interfaces.go new file mode 100644 index 0000000..950efb7 --- /dev/null +++ b/errkit/interfaces.go @@ -0,0 +1,32 @@ +package errkit + +import "encoding/json" + +var ( + _ json.Marshaler = &ErrorX{} + _ JoinedError = &ErrorX{} + _ CauseError = &ErrorX{} + _ ComparableError = &ErrorX{} + _ error = &ErrorX{} +) + +// below contains all interfaces that are implemented by ErrorX which +// makes it compatible with other error packages + +// JoinedError is implemented by errors that are joined by Join +type JoinedError interface { + // Unwrap returns the underlying error + Unwrap() []error +} + +// CauseError is implemented by errors that have a cause +type CauseError interface { + // Cause return the original error that caused this without any wrapping + Cause() error +} + +// ComparableError is implemented by errors that can be compared +type ComparableError interface { + // Is checks if current error contains given error + Is(err error) bool +} diff --git a/errkit/kind.go b/errkit/kind.go new file mode 100644 index 0000000..2d35cac --- /dev/null +++ b/errkit/kind.go @@ -0,0 +1,187 @@ +package errkit + +import ( + "strings" + + "github.com/projectdiscovery/utils/env" + "golang.org/x/exp/maps" +) + +var ( + // MaxChainedKinds is the maximum number of error kinds that can be chained in a error + MaxChainedKinds = env.GetEnvOrDefault("MAX_CHAINED_ERR_KINDS", 3) +) + +var ( + // ErrClassNetwork indicates an error related to network operations + // these may be resolved by retrying the operation with exponential backoff + // ex: Timeout awaiting headers, i/o timeout etc + ErrClassNetworkTemporary ErrKind = NewPrimitiveErrKind("network-temporary-error", "temporary network error", isNetworkTemporaryErr) + // ErrClassNetworkPermanent indicates a permanent error related to network operations + // these may not be resolved by retrying and need manual intervention + // ex: no address found for host + ErrClassNetworkPermanent = NewPrimitiveErrKind("network-permanent-error", "permanent network error", isNetworkPermanentErr) + // ErrClassDeadline indicates a timeout error in logical operations + // these are custom deadlines set by nuclei itself to prevent infinite hangs + // and in most cases are server side issues (ex: server connects but does not respond at all) + // a manual intervention is required + ErrClassDeadline = NewPrimitiveErrKind("deadline-error", "deadline error", isDeadlineErr) + // ErrClassUnknown indicates an unknown error class + // that has not been implemented yet this is used as fallback when converting a slog Item + ErrClassUnknown = NewPrimitiveErrKind("unknown-error", "unknown error", nil) +) + +// ErrKind is an interface that represents a kind of error +type ErrKind interface { + // Is checks if current error kind is same as given error kind + Is(ErrKind) bool + // IsParent checks if current error kind is parent of given error kind + // this allows heirarchical classification of errors and app specific handling + IsParent(ErrKind) bool + // RepresentsError checks if given error is of this kind + Represents(*ErrorX) bool + // Description returns predefined description of the error kind + // this can be used to show user friendly error messages in case of error + Description() string + // String returns the string representation of the error kind + String() string +} + +var _ ErrKind = &primitiveErrKind{} + +// primitiveErrKind is kind of error used in classification +type primitiveErrKind struct { + id string + info string + represents func(*ErrorX) bool +} + +func (e *primitiveErrKind) Is(kind ErrKind) bool { + return e.id == kind.String() +} + +func (e *primitiveErrKind) IsParent(kind ErrKind) bool { + return false +} + +func (e *primitiveErrKind) Represents(err *ErrorX) bool { + if e.represents != nil { + return e.represents(err) + } + return false +} + +func (e *primitiveErrKind) String() string { + return e.id +} + +func (e *primitiveErrKind) Description() string { + return e.info +} + +// NewPrimitiveErrKind creates a new primitive error kind +func NewPrimitiveErrKind(id string, info string, represents func(*ErrorX) bool) ErrKind { + p := &primitiveErrKind{id: id, info: info, represents: represents} + return p +} + +func isNetworkTemporaryErr(err *ErrorX) bool { + return false +} + +// isNetworkPermanentErr checks if given error is a permanent network error +func isNetworkPermanentErr(err *ErrorX) bool { + // to implement + return false +} + +// isDeadlineErr checks if given error is a deadline error +func isDeadlineErr(err *ErrorX) bool { + // to implement + return false +} + +type multiKind struct { + kinds []ErrKind +} + +func (e *multiKind) Is(kind ErrKind) bool { + for _, k := range e.kinds { + if k.Is(kind) { + return true + } + } + return false +} + +func (e *multiKind) IsParent(kind ErrKind) bool { + for _, k := range e.kinds { + if k.IsParent(kind) { + return true + } + } + return false +} + +func (e *multiKind) Represents(err *ErrorX) bool { + for _, k := range e.kinds { + if k.Represents(err) { + return true + } + } + return false +} + +func (e *multiKind) String() string { + var str string + for _, k := range e.kinds { + str += k.String() + "," + } + return strings.TrimSuffix(str, ",") +} + +func (e *multiKind) Description() string { + var str string + for _, k := range e.kinds { + str += k.Description() + "\n" + } + return strings.TrimSpace(str) +} + +// CombineErrKinds combines multiple error kinds into a single error kind +// this is not recommended but available if needed +// It is currently used in ErrorX while printing the error +// It is recommended to implement a hierarchical error kind +// instead of using this outside of errkit +func CombineErrKinds(kind ...ErrKind) ErrKind { + // while combining it also consolidates child error kinds into parent + // but note it currently does not support deeply nested childs + // and can only consolidate immediate childs + f := &multiKind{} + uniq := map[ErrKind]struct{}{} + for _, k := range kind { + if k == nil { + continue + } + if val, ok := k.(*multiKind); ok { + for _, v := range val.kinds { + uniq[v] = struct{}{} + } + } else { + uniq[k] = struct{}{} + } + } + all := maps.Keys(uniq) + for _, k := range all { + for u := range uniq { + if k.IsParent(u) { + delete(uniq, k) + } + } + } + f.kinds = maps.Keys(uniq) + if len(f.kinds) > MaxChainedKinds { + f.kinds = f.kinds[:MaxChainedKinds] + } + return f +} From dcbe8b1342eac34fb10cdb225af12e788c8b9bd4 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Tue, 7 May 2024 22:01:01 +0530 Subject: [PATCH 02/14] errkit: print attr on .String() --- errkit/errors.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/errkit/errors.go b/errkit/errors.go index 729b2f1..f87e228 100644 --- a/errkit/errors.go +++ b/errkit/errors.go @@ -85,6 +85,10 @@ func (e *ErrorX) Error() string { sb.WriteString(e.kind.String()) sb.WriteString(" ") } + if len(e.attrs) > 0 { + sb.WriteString(slog.GroupValue(e.attrs...).String()) + sb.WriteString(" ") + } for _, err := range e.errs { sb.WriteString(err.Error()) sb.WriteString(ErrorSeperator) From 547a96bdfd2c789cb995599dfc1785ca9dac3783 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Wed, 8 May 2024 14:21:40 +0530 Subject: [PATCH 03/14] more helper functions + improved docs --- errkit/README.md | 36 ++++++++++++------------- errkit/helpers.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++- errkit/kind.go | 10 ++----- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/errkit/README.md b/errkit/README.md index 279a163..4279e06 100644 --- a/errkit/README.md +++ b/errkit/README.md @@ -4,30 +4,30 @@ why errkit when we already have errorutil ? ---- -errorutil was introduced a year ago with main goal to capture stack of error to identify underlying deeply nested errors. but it does not follow the go paradigm of error handling and implementation and is counter intuitive to that. i.e in golang looking at any error util/implementation library "errors" "pkg/errors" or "uber.go/multierr" etc, they all follow the same pattern i.e `.Error()` method is never used and instead it is wrapped with helper structs following a particular interface which allows traversing the error chain and using helper functions like `.Cause() error` or `.Unwrap() error` or `errors.Is()` and more. but errorutil marshalls the error to string which does not play well with the go error handling paradigm. Apart from that over time usage of errorutil has been cumbersome because it is not drop in replacement for any error package and it does not allow propogating/traversing error chain in a go idiomatic way. +Introduced a year ago, `errorutil` aimed to capture error stacks for identifying deeply nested errors. However, its approach deviates from Go's error handling paradigm. In Go, libraries like "errors", "pkg/errors", and "uber.go/multierr" avoid using the `.Error()` method directly. Instead, they wrap errors with helper structs that implement specific interfaces, facilitating error chain traversal and the use of helper functions like `.Cause() error` or `.Unwrap() error` or `errors.Is()`. Contrarily, `errorutil` marshals errors to strings, which is incompatible with Go's error handling paradigm. Over time, the use of `errorutil` has become cumbersome due to its inability to replace any error package seamlessly and its lack of support for idiomatic error propagation or traversal in Go. -`errkit` is new error library that is built upon learnings from `errorutil` and has following features: +`errkit` is a new error library that addresses the shortcomings of `errorutil`. It offers the following features: -- drop in replacement for (no syntax change / refactor required) +- Seamless replacement for existing error packages, requiring no syntax changes or refactoring: - `errors` package - `pkg/errors` package (now deprecated) - `uber/multierr` package -- is compatible with all known go error handling implementations and can parse errors from any library and is compatible with existing error handling libraries and helper functions like `Is()` , `As()` , `Cause()` and more. -- is go idiomatic and follows the go error handling paradigm -- Has Attributes support (see below) -- Implements and categorizes errors into different classes (see below) - - `ErrClassNetworkTemporary` - - `ErrClassNetworkPermanent` - - `ErrClassDeadline` - - Custom Classes via `ErrClass` interface -- Supports easy conversion to slog Item for structured logging reatining all error info -- Helper functions to implement public/user facing errors by using error classes +- `errkit` is compatible with all known Go error handling implementations. It can parse errors from any library and works with existing error handling libraries and helper functions like `Is()`, `As()`, `Cause()`, and more. +- `errkit` is Go idiomatic and adheres to the Go error handling paradigm. +- `errkit` supports attributes for structured error information or logging using `slog.Attr` (optional). +- `errkit` implements and categorizes errors into different kinds, as detailed below. + - `ErrKindNetworkTemporary` + - `ErrKindNetworkPermanent` + - `ErrKindDeadline` + - Custom kinds via `ErrKind` interface +- `errkit` provides helper functions for structured error logging using `SlogAttrs` and `SlogAttrGroup`. +- `errkit` offers helper functions to implement public or user-facing errors by using error kinds interface. **Attributes Support** -To strictly follow the go error handling paradigm and making it easy to traverse error chain, errkit support adding `Attr(key comparable,value any)` to error instead of wrapping it with a string message. This keeps extra error info minimal without being too verbose a good example of this is following +`errkit` supports optional error wrapping with attributes `slog.Attr` for structured error logging, providing a more organized approach to error logging than string wrapping. ```go // normal way of error propogating through nested stack @@ -47,12 +47,12 @@ with attributes support you can do following err := errkit.New("i/o timeout") // xyz.go -err = errkit.WithAttr(err,&errkit.Resource{},errkit.Resource{type: "network",add: addr}) +err = errkit.WithAttr(err,slog.Any("resource",domain)) // abc.go -err = errkit.WithAttr(err,&errkit.Action{},errkit.Action{type: "download"}) +err = errkit.WithAttr(err,slog.Any("action","download")) ``` -the good part is that all attributes must implement a interface 'ErrAttr' that way all of them are compatible and can be consolidated into a struct/object of choice. for more example see `attr_test.go` +## Note -In case the same attribute types are added they are deduplicated / consolidated into parent attribute if it supports that. \ No newline at end of file +To keep errors concise and avoid unnecessary allocations, message wrapping and attributes count have a max depth set to 3. Adding more will not panic but will be simply ignored. This is configurable using the MAX_ERR_DEPTH env variable (default 3). \ No newline at end of file diff --git a/errkit/helpers.go b/errkit/helpers.go index 4150fb6..b3a8990 100644 --- a/errkit/helpers.go +++ b/errkit/helpers.go @@ -1,6 +1,9 @@ package errkit -import "errors" +import ( + "errors" + "log/slog" +) // Proxy to StdLib errors.Is func Is(err error, target ...error) bool { @@ -127,3 +130,65 @@ func IsNetworkTemporaryErr(err error) bool { parseError(x, err) return isNetworkTemporaryErr(x) } + +// WithAttr wraps error with given attributes +// +// err = errkit.WithAttr(err,slog.Any("resource",domain)) +func WithAttr(err error, attrs ...slog.Attr) error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + x.attrs = append(x.attrs, attrs...) + if len(x.attrs) > MaxErrorDepth { + x.attrs = x.attrs[:MaxErrorDepth] + } + return x +} + +// SlogAttrGroup returns a slog attribute group for the given error +// it is in format of: +// +// { +// "data": { +// "kind": "", +// "cause": "", +// "errors": [ +// ... +// ] +// } +// } +func SlogAttrGroup(err error) slog.Attr { + attrs := SlogAttrs(err) + g := slog.GroupValue( + attrs..., // append all attrs + ) + return slog.Any("data", g) +} + +// SlogAttrs returns slog attributes for the given error +// it is in format of: +// +// { +// "kind": "", +// "cause": "", +// "errors": [ +// ... +// ] +// } +func SlogAttrs(err error) []slog.Attr { + x := &ErrorX{} + parseError(x, err) + attrs := []slog.Attr{} + if x.kind != nil { + attrs = append(attrs, slog.Any("kind", x.kind.String())) + } + if cause := x.Cause(); cause != nil { + attrs = append(attrs, slog.Any("cause", cause)) + } + if len(x.errs) > 0 { + attrs = append(attrs, slog.Any("errors", x.errs)) + } + return attrs +} diff --git a/errkit/kind.go b/errkit/kind.go index 2d35cac..7fef63f 100644 --- a/errkit/kind.go +++ b/errkit/kind.go @@ -3,15 +3,9 @@ package errkit import ( "strings" - "github.com/projectdiscovery/utils/env" "golang.org/x/exp/maps" ) -var ( - // MaxChainedKinds is the maximum number of error kinds that can be chained in a error - MaxChainedKinds = env.GetEnvOrDefault("MAX_CHAINED_ERR_KINDS", 3) -) - var ( // ErrClassNetwork indicates an error related to network operations // these may be resolved by retrying the operation with exponential backoff @@ -180,8 +174,8 @@ func CombineErrKinds(kind ...ErrKind) ErrKind { } } f.kinds = maps.Keys(uniq) - if len(f.kinds) > MaxChainedKinds { - f.kinds = f.kinds[:MaxChainedKinds] + if len(f.kinds) > MaxErrorDepth { + f.kinds = f.kinds[:MaxErrorDepth] } return f } From c77d95f5cb2e0ff6e394828241682587f836b363 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Thu, 16 May 2024 00:32:23 +0530 Subject: [PATCH 04/14] remove unused/dead code --- errkit/attr.go | 19 ------------------- errkit/errors_test.go | 1 - errkit/kind.go | 2 +- 3 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 errkit/attr.go diff --git a/errkit/attr.go b/errkit/attr.go deleted file mode 100644 index 602c9bc..0000000 --- a/errkit/attr.go +++ /dev/null @@ -1,19 +0,0 @@ -package errkit - -// AttrResource is a resource attribute -// which can be used to represent a resource -type AttrResource struct { - // Type is the type of resource - Type string - // Value is the value of resource - Value string -} - -// AttrAction is an action attribute -// which can be used to represent an action -type AttrAction struct { - // Type is the type of action - Type string - // Value is the value of action - Value string -} diff --git a/errkit/errors_test.go b/errkit/errors_test.go index 824e699..e5e1a7d 100644 --- a/errkit/errors_test.go +++ b/errkit/errors_test.go @@ -76,5 +76,4 @@ func TestErrorUtil(t *testing.T) { if len(x.errs) != 3 { t.Fatal("expected 3 errors") } - t.Log(x.errs) } diff --git a/errkit/kind.go b/errkit/kind.go index 7fef63f..52f5d47 100644 --- a/errkit/kind.go +++ b/errkit/kind.go @@ -168,7 +168,7 @@ func CombineErrKinds(kind ...ErrKind) ErrKind { all := maps.Keys(uniq) for _, k := range all { for u := range uniq { - if k.IsParent(u) { + if k.IsParent(u) || k.Is(u) { delete(uniq, k) } } From d217f19e00c88fd96233aa700b17b998a0250e3d Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Thu, 16 May 2024 00:44:00 +0530 Subject: [PATCH 05/14] skip random zero on windows --- slice/sliceutil_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/slice/sliceutil_test.go b/slice/sliceutil_test.go index 178540b..712d3a1 100644 --- a/slice/sliceutil_test.go +++ b/slice/sliceutil_test.go @@ -3,6 +3,7 @@ package sliceutil import ( "testing" + osutils "github.com/projectdiscovery/utils/os" "github.com/stretchr/testify/require" ) @@ -287,6 +288,10 @@ func TestVisitRandom(t *testing.T) { } func TestVisitRandomZero(t *testing.T) { + // skipped on windows due to flakiness + if osutils.IsWindows() { + t.Skip("skipping test on windows") + } intSlice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} var timesDifferent int for i := 0; i < 100; i++ { From 7cf6da23c0c98c2be1a5dd094b69a741e8d89014 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Thu, 16 May 2024 01:28:31 +0530 Subject: [PATCH 06/14] add common error kinds --- errkit/kind.go | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/errkit/kind.go b/errkit/kind.go index 52f5d47..455af0c 100644 --- a/errkit/kind.go +++ b/errkit/kind.go @@ -1,6 +1,9 @@ package errkit import ( + "context" + "errors" + "os" "strings" "golang.org/x/exp/maps" @@ -80,18 +83,53 @@ func NewPrimitiveErrKind(id string, info string, represents func(*ErrorX) bool) } func isNetworkTemporaryErr(err *ErrorX) bool { + if err.Cause() != nil { + return os.IsTimeout(err.Cause()) + } + v := err.Cause() + switch { + case os.IsTimeout(v): + return true + case strings.Contains(v.Error(), "Client.Timeout exceeded while awaiting headers"): + return true + } return false } // isNetworkPermanentErr checks if given error is a permanent network error func isNetworkPermanentErr(err *ErrorX) bool { + if err.Cause() == nil { + return false + } + v := err.Cause().Error() // to implement + switch { + case strings.Contains(v, "no address found"): + return true + case strings.Contains(v, "no such host"): + return true + case strings.Contains(v, "could not resolve host"): + return true + } return false } // isDeadlineErr checks if given error is a deadline error func isDeadlineErr(err *ErrorX) bool { // to implement + if err.Cause() == nil { + return false + } + v := err.Cause() + switch { + case errors.Is(v, os.ErrDeadlineExceeded): + return true + case errors.Is(v, context.DeadlineExceeded): + return true + case errors.Is(v, context.Canceled): + return true + } + return false } @@ -168,7 +206,7 @@ func CombineErrKinds(kind ...ErrKind) ErrKind { all := maps.Keys(uniq) for _, k := range all { for u := range uniq { - if k.IsParent(u) || k.Is(u) { + if k.IsParent(u) || k.Is(u) { delete(uniq, k) } } From e88a2ee117634dca60eda8e38ef6b42d7df069d8 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Thu, 16 May 2024 01:31:34 +0530 Subject: [PATCH 07/14] add IsDeadlineErr , IsNetworkPermanentErr --- errkit/helpers.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/errkit/helpers.go b/errkit/helpers.go index b3a8990..2401cb6 100644 --- a/errkit/helpers.go +++ b/errkit/helpers.go @@ -131,6 +131,26 @@ func IsNetworkTemporaryErr(err error) bool { return isNetworkTemporaryErr(x) } +// IsDeadlineErr checks if given error is a deadline error +func IsDeadlineErr(err error) bool { + if err == nil { + return false + } + x := &ErrorX{} + parseError(x, err) + return isDeadlineErr(x) +} + +// IsNetworkPermanentErr checks if given error is a permanent network error +func IsNetworkPermanentErr(err error) bool { + if err == nil { + return false + } + x := &ErrorX{} + parseError(x, err) + return isNetworkPermanentErr(x) +} + // WithAttr wraps error with given attributes // // err = errkit.WithAttr(err,slog.Any("resource",domain)) From 68f989385887a79357c651c49910b94a610aa3ee Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Thu, 16 May 2024 02:09:11 +0530 Subject: [PATCH 08/14] add kind helpers + fix kind name --- errkit/errors_test.go | 4 +-- errkit/kind.go | 60 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/errkit/errors_test.go b/errkit/errors_test.go index e5e1a7d..e63eb63 100644 --- a/errkit/errors_test.go +++ b/errkit/errors_test.go @@ -16,7 +16,7 @@ import ( func TestErrorAs(t *testing.T) { // Create a new error with a specific class and wrap it - x := New("this is a nuclei error").SetKind(ErrClassNetworkPermanent).Build() + x := New("this is a nuclei error").SetKind(ErrKindNetworkPermanent).Build() y := errors.Wrap(x, "this is a wrap error") // Attempt to unwrap the error to a specific type @@ -45,7 +45,7 @@ func TestErrorAs(t *testing.T) { func TestErrorIs(t *testing.T) { // Create a new error, wrap it, and check if the original error can be found - x := New("this is a nuclei error").SetKind(ErrClassNetworkPermanent).Build() + x := New("this is a nuclei error").SetKind(ErrKindNetworkPermanent).Build() y := errors.Wrap(x, "this is a wrap error") if !errors.Is(y, x) { t.Fatal("expected to be able to find the original error") diff --git a/errkit/kind.go b/errkit/kind.go index 455af0c..a01fd13 100644 --- a/errkit/kind.go +++ b/errkit/kind.go @@ -13,19 +13,19 @@ var ( // ErrClassNetwork indicates an error related to network operations // these may be resolved by retrying the operation with exponential backoff // ex: Timeout awaiting headers, i/o timeout etc - ErrClassNetworkTemporary ErrKind = NewPrimitiveErrKind("network-temporary-error", "temporary network error", isNetworkTemporaryErr) - // ErrClassNetworkPermanent indicates a permanent error related to network operations + ErrKindNetworkTemporary = NewPrimitiveErrKind("network-temporary-error", "temporary network error", isNetworkTemporaryErr) + // ErrKindNetworkPermanent indicates a permanent error related to network operations // these may not be resolved by retrying and need manual intervention // ex: no address found for host - ErrClassNetworkPermanent = NewPrimitiveErrKind("network-permanent-error", "permanent network error", isNetworkPermanentErr) - // ErrClassDeadline indicates a timeout error in logical operations + ErrKindNetworkPermanent = NewPrimitiveErrKind("network-permanent-error", "permanent network error", isNetworkPermanentErr) + // ErrKindDeadline indicates a timeout error in logical operations // these are custom deadlines set by nuclei itself to prevent infinite hangs // and in most cases are server side issues (ex: server connects but does not respond at all) // a manual intervention is required - ErrClassDeadline = NewPrimitiveErrKind("deadline-error", "deadline error", isDeadlineErr) - // ErrClassUnknown indicates an unknown error class + ErrKindDeadline = NewPrimitiveErrKind("deadline-error", "deadline error", isDeadlineErr) + // ErrKindUnknown indicates an unknown error class // that has not been implemented yet this is used as fallback when converting a slog Item - ErrClassUnknown = NewPrimitiveErrKind("unknown-error", "unknown error", nil) + ErrKindUnknown = NewPrimitiveErrKind("unknown-error", "unknown error", nil) ) // ErrKind is an interface that represents a kind of error @@ -217,3 +217,49 @@ func CombineErrKinds(kind ...ErrKind) ErrKind { } return f } + +// GetErrorKind returns the first error kind from the error +// extra error kinds can be passed as optional arguments +func GetErrorKind(err error, defs ...ErrKind) ErrKind { + x := &ErrorX{} + parseError(x, err) + if x.kind != nil { + if val, ok := x.kind.(*multiKind); ok { + // if multi kind return first kind + return val.kinds[0] + } + return x.kind + } + // if no kind is found return default error kind + // parse if defs are given + for _, def := range defs { + if def.Represents(x) { + return def + } + } + return ErrKindUnknown +} + +// GetAllErrorKinds returns all error kinds from the error +// this should not be used unless very good reason to do so +func GetAllErrorKinds(err error, defs ...ErrKind) []ErrKind { + kinds := []ErrKind{} + x := &ErrorX{} + parseError(x, err) + if x.kind != nil { + if val, ok := x.kind.(*multiKind); ok { + kinds = append(kinds, val.kinds...) + } else { + kinds = append(kinds, x.kind) + } + } + for _, def := range defs { + if def.Represents(x) { + kinds = append(kinds, def) + } + } + if len(kinds) == 0 { + kinds = append(kinds, ErrKindUnknown) + } + return kinds +} From 37bc84ef0c3fa3bd6bebefc069ffb35edf81c81c Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Thu, 16 May 2024 02:24:00 +0530 Subject: [PATCH 09/14] more helper methods --- errkit/errors.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/errkit/errors.go b/errkit/errors.go index f87e228..75b61cd 100644 --- a/errkit/errors.go +++ b/errkit/errors.go @@ -42,6 +42,16 @@ type ErrorX struct { errs []error } +// Errors returns all errors parsed by the error +func (e *ErrorX) Errors() []error { + return e.errs +} + +// Attrs returns all attributes associated with the error +func (e *ErrorX) Attrs() []slog.Attr { + return e.attrs +} + // Build returns the object as error interface func (e *ErrorX) Build() error { return e From c4bf7513228cc03eb8407e2bf7e03c8b44561895 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Mon, 20 May 2024 00:30:12 +0530 Subject: [PATCH 10/14] fix minor parsing issue --- errkit/errors.go | 15 +++++++++++++-- errkit/kind.go | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/errkit/errors.go b/errkit/errors.go index 75b61cd..afaf610 100644 --- a/errkit/errors.go +++ b/errkit/errors.go @@ -184,9 +184,20 @@ func parseError(to *ErrorX, err error) { to.errs = append(to.errs, v.errs...) to.kind = CombineErrKinds(to.kind, v.kind) case JoinedError: - to.errs = append(to.errs, v.Unwrap()...) + foundAny := false + for _, e := range v.Unwrap() { + to.errs = append(to.errs, e) + foundAny = true + } + if !foundAny { + parseError(to, errors.New(err.Error())) + } case WrappedError: - to.errs = append(to.errs, v.Unwrap()) + if v.Unwrap() != nil { + parseError(to, v.Unwrap()) + } else { + parseError(to, errors.New(err.Error())) + } case CauseError: to.errs = append(to.errs, v.Cause()) remaining := strings.Replace(err.Error(), v.Cause().Error(), "", -1) diff --git a/errkit/kind.go b/errkit/kind.go index a01fd13..3c586ca 100644 --- a/errkit/kind.go +++ b/errkit/kind.go @@ -224,7 +224,7 @@ func GetErrorKind(err error, defs ...ErrKind) ErrKind { x := &ErrorX{} parseError(x, err) if x.kind != nil { - if val, ok := x.kind.(*multiKind); ok { + if val, ok := x.kind.(*multiKind); ok && len(val.kinds) > 0 { // if multi kind return first kind return val.kinds[0] } From 7f0c6d4b25a0a4a3b59ad7ed3991646da607b1d9 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Tue, 21 May 2024 00:45:31 +0530 Subject: [PATCH 11/14] attribute improvements --- errkit/errors.go | 31 +++++++++++++------ errkit/helpers.go | 77 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 85 insertions(+), 23 deletions(-) diff --git a/errkit/errors.go b/errkit/errors.go index afaf610..79d4521 100644 --- a/errkit/errors.go +++ b/errkit/errors.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/projectdiscovery/utils/env" + "golang.org/x/exp/maps" ) const ( @@ -38,7 +39,7 @@ var ( // which can be shown to client/users in more meaningful way type ErrorX struct { kind ErrKind - attrs []slog.Attr + attrs map[string]slog.Attr errs []error } @@ -49,7 +50,10 @@ func (e *ErrorX) Errors() []error { // Attrs returns all attributes associated with the error func (e *ErrorX) Attrs() []slog.Attr { - return e.attrs + if e.attrs == nil { + return nil + } + return maps.Values(e.attrs) } // Build returns the object as error interface @@ -74,7 +78,6 @@ func (e *ErrorX) Is(err error) bool { } } } - return false } @@ -84,6 +87,9 @@ func (e *ErrorX) MarshalJSON() ([]byte, error) { "kind": e.kind.String(), "errors": e.errs, } + if len(e.attrs) > 0 { + m["attrs"] = slog.GroupValue(maps.Values(e.attrs)...) + } return json.Marshal(m) } @@ -96,7 +102,7 @@ func (e *ErrorX) Error() string { sb.WriteString(" ") } if len(e.attrs) > 0 { - sb.WriteString(slog.GroupValue(e.attrs...).String()) + sb.WriteString(slog.GroupValue(maps.Values(e.attrs)...).String()) sb.WriteString(" ") } for _, err := range e.errs { @@ -157,13 +163,18 @@ func (e *ErrorX) SetKind(kind ErrKind) *ErrorX { } // SetAttr sets additional attributes to a given error -// Note: key must be comparable and key and value both cannot -// be nil similar to context.WithValue -func (e *ErrorX) SetAttr(s slog.Attr) *ErrorX { - if len(e.attrs) >= MaxErrorDepth { - return e +// it only adds unique attributes and ignores duplicates +// Note: only key is checked for uniqueness +func (e *ErrorX) SetAttr(s ...slog.Attr) *ErrorX { + for _, attr := range s { + if e.attrs == nil { + e.attrs = make(map[string]slog.Attr) + } + // check if this exists + if _, ok := e.attrs[attr.Key]; !ok && len(e.attrs) < MaxErrorDepth { + e.attrs[attr.Key] = attr + } } - e.attrs = append(e.attrs, s) return e } diff --git a/errkit/helpers.go b/errkit/helpers.go index 2401cb6..276b171 100644 --- a/errkit/helpers.go +++ b/errkit/helpers.go @@ -10,10 +10,8 @@ func Is(err error, target ...error) bool { if err == nil { return false } - for _, t := range target { - if t == nil { - continue - } + for i := range target { + t := target[i] if errors.Is(err, t) { return true } @@ -21,6 +19,42 @@ func Is(err error, target ...error) bool { return false } +// IsKind checks if given error is equal to one of the given errkind +// if error did not already have a kind, it tries to parse it +// using default error kinds and given kinds +func IsKind(err error, match ...ErrKind) bool { + if err == nil { + return false + } + x := &ErrorX{} + parseError(x, err) + // try to parse kind from error + if x.kind == nil { + // parse kind from error + tmp := []ErrKind{ErrKindDeadline, ErrKindNetworkPermanent, ErrKindNetworkTemporary} + tmp = append(tmp, match...) + x.kind = GetErrorKind(err, tmp...) + } + if x.kind != nil { + if val, ok := x.kind.(*multiKind); ok && len(val.kinds) > 0 { + // if multi kind return first kind + for _, kind := range val.kinds { + for _, k := range match { + if k.Is(kind) { + return true + } + } + } + } + for _, kind := range match { + if kind.Is(x.kind) { + return true + } + } + } + return false +} + // Proxy to StdLib errors.As func As(err error, target interface{}) bool { return errors.As(err, target) @@ -89,6 +123,14 @@ func Append(errs ...error) error { return x } +// Join joins given errors and returns a new error +// it ignores all nil errors +// Note: unlike Other libraries, Join does not use `\n` +// so it is equivalent to wrapping/Appending errors +func Join(errs ...error) error { + return Append(errs...) +} + // Cause returns the original error that caused this error func Cause(err error) error { if err == nil { @@ -158,16 +200,25 @@ func WithAttr(err error, attrs ...slog.Attr) error { if err == nil { return nil } + if len(attrs) == 0 { + return err + } x := &ErrorX{} parseError(x, err) - x.attrs = append(x.attrs, attrs...) - if len(x.attrs) > MaxErrorDepth { - x.attrs = x.attrs[:MaxErrorDepth] + return x.SetAttr(attrs...) +} + +// GetAttr returns all attributes of given error if it has any +func GetAttr(err error) []slog.Attr { + if err == nil { + return nil } - return x + x := &ErrorX{} + parseError(x, err) + return x.Attrs() } -// SlogAttrGroup returns a slog attribute group for the given error +// ToSlogAttrGroup returns a slog attribute group for the given error // it is in format of: // // { @@ -179,15 +230,15 @@ func WithAttr(err error, attrs ...slog.Attr) error { // ] // } // } -func SlogAttrGroup(err error) slog.Attr { - attrs := SlogAttrs(err) +func ToSlogAttrGroup(err error) slog.Attr { + attrs := ToSlogAttrs(err) g := slog.GroupValue( attrs..., // append all attrs ) return slog.Any("data", g) } -// SlogAttrs returns slog attributes for the given error +// ToSlogAttrs returns slog attributes for the given error // it is in format of: // // { @@ -197,7 +248,7 @@ func SlogAttrGroup(err error) slog.Attr { // ... // ] // } -func SlogAttrs(err error) []slog.Attr { +func ToSlogAttrs(err error) []slog.Attr { x := &ErrorX{} parseError(x, err) attrs := []slog.Attr{} From 9d0d800defc6ae77d13ce84a3abd2bc8fa0503ea Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Wed, 22 May 2024 21:52:15 +0530 Subject: [PATCH 12/14] uniq attributes only --- errkit/errors.go | 10 +++++++--- errkit/helpers.go | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/errkit/errors.go b/errkit/errors.go index 79d4521..4ce8ba2 100644 --- a/errkit/errors.go +++ b/errkit/errors.go @@ -96,7 +96,7 @@ func (e *ErrorX) MarshalJSON() ([]byte, error) { // Error returns the error string func (e *ErrorX) Error() string { var sb strings.Builder - if e.kind != nil { + if e.kind != nil && e.kind.String() != "" { sb.WriteString("errKind=") sb.WriteString(e.kind.String()) sb.WriteString(" ") @@ -105,9 +105,13 @@ func (e *ErrorX) Error() string { sb.WriteString(slog.GroupValue(maps.Values(e.attrs)...).String()) sb.WriteString(" ") } + uniq := make(map[string]struct{}) for _, err := range e.errs { - sb.WriteString(err.Error()) - sb.WriteString(ErrorSeperator) + if _, ok := uniq[err.Error()]; !ok { + uniq[err.Error()] = struct{}{} + sb.WriteString(err.Error()) + sb.WriteString(ErrorSeperator) + } } return strings.TrimSuffix(sb.String(), ErrorSeperator) } diff --git a/errkit/helpers.go b/errkit/helpers.go index 276b171..c1016a0 100644 --- a/errkit/helpers.go +++ b/errkit/helpers.go @@ -263,3 +263,18 @@ func ToSlogAttrs(err error) []slog.Attr { } return attrs } + +// GetAttrValue returns the value of the attribute with given key +func GetAttrValue(err error, key string) slog.Value { + if err == nil { + return slog.Value{} + } + x := &ErrorX{} + parseError(x, err) + for _, attr := range x.attrs { + if attr.Key == key { + return attr.Value + } + } + return slog.Value{} +} From 5361171e1ec0f2112ea1c4061fce8c8c11eecf87 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Wed, 22 May 2024 22:02:47 +0530 Subject: [PATCH 13/14] uniq errors only --- errkit/errors.go | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/errkit/errors.go b/errkit/errors.go index 4ce8ba2..fe93b63 100644 --- a/errkit/errors.go +++ b/errkit/errors.go @@ -38,9 +38,25 @@ var ( // wrapping and joining strategies including custom ones and it supports error class // which can be shown to client/users in more meaningful way type ErrorX struct { - kind ErrKind - attrs map[string]slog.Attr - errs []error + kind ErrKind + attrs map[string]slog.Attr + errs []error + uniqErrs map[string]struct{} +} + +// append is internal method to append given +// error to error slice , it removes duplicates +func (e *ErrorX) append(errs ...error) { + if e.uniqErrs == nil { + e.uniqErrs = make(map[string]struct{}) + } + for _, err := range errs { + if _, ok := e.uniqErrs[err.Error()]; ok { + continue + } + e.uniqErrs[err.Error()] = struct{}{} + e.errs = append(e.errs, err) + } } // Errors returns all errors parsed by the error @@ -105,13 +121,9 @@ func (e *ErrorX) Error() string { sb.WriteString(slog.GroupValue(maps.Values(e.attrs)...).String()) sb.WriteString(" ") } - uniq := make(map[string]struct{}) for _, err := range e.errs { - if _, ok := uniq[err.Error()]; !ok { - uniq[err.Error()] = struct{}{} - sb.WriteString(err.Error()) - sb.WriteString(ErrorSeperator) - } + sb.WriteString(err.Error()) + sb.WriteString(ErrorSeperator) } return strings.TrimSuffix(sb.String(), ErrorSeperator) } @@ -127,6 +139,9 @@ func (e *ErrorX) Cause() error { // Kind returns the errorkind associated with this error // if any func (e *ErrorX) Kind() ErrKind { + if e.kind == nil || e.kind.String() == "" { + return ErrKindUnknown + } return e.kind } @@ -151,7 +166,7 @@ func (e *ErrorX) Msgf(format string, args ...interface{}) { if e == nil { return } - e.errs = append(e.errs, fmt.Errorf(format, args...)) + e.append(fmt.Errorf(format, args...)) } // SetClass sets the class of the error @@ -196,12 +211,12 @@ func parseError(to *ErrorX, err error) { switch v := err.(type) { case *ErrorX: - to.errs = append(to.errs, v.errs...) + to.append(v.errs...) to.kind = CombineErrKinds(to.kind, v.kind) case JoinedError: foundAny := false for _, e := range v.Unwrap() { - to.errs = append(to.errs, e) + to.append(e) foundAny = true } if !foundAny { @@ -214,7 +229,7 @@ func parseError(to *ErrorX, err error) { parseError(to, errors.New(err.Error())) } case CauseError: - to.errs = append(to.errs, v.Cause()) + to.append(v.Cause()) remaining := strings.Replace(err.Error(), v.Cause().Error(), "", -1) parseError(to, errors.New(remaining)) default: @@ -251,7 +266,7 @@ func parseError(to *ErrorX, err error) { } } else { // this cannot be furthur unwrapped - to.errs = append(to.errs, err) + to.append(err) } } } From 389764fb85e2f64490474159c4a2b5aad4a7b51e Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Thu, 23 May 2024 02:04:54 +0530 Subject: [PATCH 14/14] bug fix error kind --- errkit/errors_test.go | 28 ++++++++++++++++++++++++++++ errkit/kind.go | 38 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/errkit/errors_test.go b/errkit/errors_test.go index e63eb63..f83db4a 100644 --- a/errkit/errors_test.go +++ b/errkit/errors_test.go @@ -5,6 +5,7 @@ import ( "github.com/pkg/errors" errorutil "github.com/projectdiscovery/utils/errors" + "github.com/stretchr/testify/require" "go.uber.org/multierr" stderrors "errors" @@ -77,3 +78,30 @@ func TestErrorUtil(t *testing.T) { t.Fatal("expected 3 errors") } } + +func TestErrKindCheck(t *testing.T) { + x := New("port closed or filtered").SetKind(ErrKindNetworkPermanent) + t.Run("Errkind With Normal Error", func(t *testing.T) { + wrapped := Wrap(x, "this is a wrapped error") + if !IsKind(wrapped, ErrKindNetworkPermanent) { + t.Fatal("expected to be able to find the original error") + } + }) + + // mix of multiple kinds + tmp := New("i/o timeout").SetKind(ErrKindNetworkTemporary) + t.Run("Errkind With Multiple Kinds", func(t *testing.T) { + wrapped := Append(x, tmp) + errx := FromError(wrapped) + val, ok := errx.kind.(*multiKind) + require.True(t, ok, "expected to be able to find the original error") + require.Equal(t, 2, len(val.kinds)) + }) + + // duplicate kinds + t.Run("Errkind With Duplicate Kinds", func(t *testing.T) { + wrapped := Append(x, x) + errx := FromError(wrapped) + require.True(t, errx.kind.Is(ErrKindNetworkPermanent), "expected to be able to find the original error") + }) +} diff --git a/errkit/kind.go b/errkit/kind.go index 3c586ca..9e3cd6c 100644 --- a/errkit/kind.go +++ b/errkit/kind.go @@ -28,6 +28,17 @@ var ( ErrKindUnknown = NewPrimitiveErrKind("unknown-error", "unknown error", nil) ) +var ( + // DefaultErrorKinds is the default error kinds used in classification + // if one intends to add more default error kinds it must be done in init() function + // of that package to avoid race conditions + DefaultErrorKinds = []ErrKind{ + ErrKindNetworkTemporary, + ErrKindNetworkPermanent, + ErrKindDeadline, + } +) + // ErrKind is an interface that represents a kind of error type ErrKind interface { // Is checks if current error kind is same as given error kind @@ -110,6 +121,11 @@ func isNetworkPermanentErr(err *ErrorX) bool { return true case strings.Contains(v, "could not resolve host"): return true + case strings.Contains(v, "port closed or filtered"): + // pd standard error for port closed or filtered + return true + case strings.Contains(v, "connect: connection refused"): + return true } return false } @@ -192,7 +208,7 @@ func CombineErrKinds(kind ...ErrKind) ErrKind { f := &multiKind{} uniq := map[ErrKind]struct{}{} for _, k := range kind { - if k == nil { + if k == nil || k.String() == "" { continue } if val, ok := k.(*multiKind); ok { @@ -206,15 +222,27 @@ func CombineErrKinds(kind ...ErrKind) ErrKind { all := maps.Keys(uniq) for _, k := range all { for u := range uniq { - if k.IsParent(u) || k.Is(u) { + if k.IsParent(u) { + delete(uniq, u) + } + } + } + if len(uniq) > 1 { + // check and remove unknown error kind + for k := range uniq { + if k.Is(ErrKindUnknown) { delete(uniq, k) } } } + f.kinds = maps.Keys(uniq) if len(f.kinds) > MaxErrorDepth { f.kinds = f.kinds[:MaxErrorDepth] } + if len(f.kinds) == 1 { + return f.kinds[0] + } return f } @@ -237,6 +265,12 @@ func GetErrorKind(err error, defs ...ErrKind) ErrKind { return def } } + // check in default error kinds + for _, def := range DefaultErrorKinds { + if def.Represents(x) { + return def + } + } return ErrKindUnknown }