-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #416 from projectdiscovery/feat-errkit
Feat errkit
- Loading branch information
Showing
7 changed files
with
1,059 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# errkit | ||
|
||
why errkit when we already have errorutil ? | ||
|
||
---- | ||
|
||
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 a new error library that addresses the shortcomings of `errorutil`. It offers the following features: | ||
|
||
- Seamless replacement for existing error packages, requiring no syntax changes or refactoring: | ||
- `errors` package | ||
- `pkg/errors` package (now deprecated) | ||
- `uber/multierr` package | ||
- `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** | ||
|
||
`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 | ||
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,slog.Any("resource",domain)) | ||
|
||
// abc.go | ||
err = errkit.WithAttr(err,slog.Any("action","download")) | ||
``` | ||
|
||
## Note | ||
|
||
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,278 @@ | ||
// 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" | ||
"golang.org/x/exp/maps" | ||
) | ||
|
||
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 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 | ||
func (e *ErrorX) Errors() []error { | ||
return e.errs | ||
} | ||
|
||
// Attrs returns all attributes associated with the error | ||
func (e *ErrorX) Attrs() []slog.Attr { | ||
if e.attrs == nil { | ||
return nil | ||
} | ||
return maps.Values(e.attrs) | ||
} | ||
|
||
// 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, | ||
} | ||
if len(e.attrs) > 0 { | ||
m["attrs"] = slog.GroupValue(maps.Values(e.attrs)...) | ||
} | ||
return json.Marshal(m) | ||
} | ||
|
||
// Error returns the error string | ||
func (e *ErrorX) Error() string { | ||
var sb strings.Builder | ||
if e.kind != nil && e.kind.String() != "" { | ||
sb.WriteString("errKind=") | ||
sb.WriteString(e.kind.String()) | ||
sb.WriteString(" ") | ||
} | ||
if len(e.attrs) > 0 { | ||
sb.WriteString(slog.GroupValue(maps.Values(e.attrs)...).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 { | ||
if e.kind == nil || e.kind.String() == "" { | ||
return ErrKindUnknown | ||
} | ||
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.append(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 | ||
// 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 | ||
} | ||
} | ||
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.append(v.errs...) | ||
to.kind = CombineErrKinds(to.kind, v.kind) | ||
case JoinedError: | ||
foundAny := false | ||
for _, e := range v.Unwrap() { | ||
to.append(e) | ||
foundAny = true | ||
} | ||
if !foundAny { | ||
parseError(to, errors.New(err.Error())) | ||
} | ||
case WrappedError: | ||
if v.Unwrap() != nil { | ||
parseError(to, v.Unwrap()) | ||
} else { | ||
parseError(to, errors.New(err.Error())) | ||
} | ||
case CauseError: | ||
to.append(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.append(err) | ||
} | ||
} | ||
} | ||
|
||
// WrappedError is implemented by errors that are wrapped | ||
type WrappedError interface { | ||
// Unwrap returns the underlying error | ||
Unwrap() error | ||
} |
Oops, something went wrong.