Compare commits

...

1 Commits

Author SHA1 Message Date
Tushar Vats
03112ebd5b fix: added additional fields in errors pkg 2026-05-25 22:40:33 +05:30
23 changed files with 500 additions and 109 deletions

View File

@@ -2391,6 +2391,8 @@ components:
type: object
ErrorsJSON:
properties:
attrs:
$ref: '#/components/schemas/ErrorsResponseattributes'
code:
type: string
errors:
@@ -2399,17 +2401,45 @@ components:
type: array
message:
type: string
retry:
$ref: '#/components/schemas/ErrorsResponseretryjson'
type:
type: string
url:
type: string
required:
- type
- code
- message
type: object
ErrorsResponseattributes:
additionalProperties: {}
type: object
ErrorsResponseerroradditional:
properties:
message:
type: string
type: object
ErrorsResponseretryjson:
properties:
after:
description: Retry delay in google.protobuf.Duration JSON form. Present
only when policy is "after".
example: 5.5s
format: duration
type: string
policy:
$ref: '#/components/schemas/ErrorsResponseretrypolicy'
type: object
ErrorsResponseretrypolicy:
enum:
- never
- immediate
- backoff
- after
- after_fix
- after_auth
type: string
FactoryResponse:
properties:
healthy:

View File

@@ -894,7 +894,7 @@ func TestEmailGetPassword(t *testing.T) {
if len(tc.errMsg) > 0 {
require.Error(t, err)
if errors.Asc(err, errors.CodeInternal) {
_, _, errMsg, _, _, _ := errors.Unwrapb(err)
_, _, errMsg, _, _, _, _, _ := errors.Unwrapb(err)
require.Contains(t, errMsg, tc.errMsg)
} else {
require.Contains(t, err.Error(), tc.errMsg)

View File

@@ -327,7 +327,7 @@ func TestPagerDutyTemplating(t *testing.T) {
} else {
require.Error(t, err)
if errors.Asc(err, errors.CodeInternal) {
_, _, errMsg, _, _, _ := errors.Unwrapb(err)
_, _, errMsg, _, _, _, _, _ := errors.Unwrapb(err)
require.Contains(t, errMsg, tc.errMsg)
} else {
require.Contains(t, err.Error(), tc.errMsg)

View File

@@ -4,12 +4,13 @@ import (
"errors" //nolint:depguard
"fmt"
"log/slog"
"maps"
"time"
"go.opentelemetry.io/otel/attribute"
)
// base is the fundamental struct that implements the error interface.
// The order of the struct is 'TCMEUAS'.
type base struct {
// t denotes the custom type of the error.
t typ
@@ -22,9 +23,15 @@ type base struct {
// u denotes the url for the documentation (if present) for the error.
u string
// a denotes any additional error messages (if present).
// NOTE: use attrs['suggestions'] for additional structured suggestions instead of this field.
a []string
// s contains the stacktrace captured at error creation time.
s fmt.Stringer
// r is the retry strategy for the error, if applicable.
r retry
// attrs contains any additional attributes for the error, if present.
// attrs is declared to be map[string]any, however map[string][]string is expected to be used for values.
attrs map[string]any
}
// Stacktrace returns the stacktrace captured at error creation time, formatted as a string.
@@ -39,13 +46,15 @@ func (b *base) Stacktrace() string {
// and returns a new base error.
func (b *base) WithStacktrace(s string) *base {
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: b.a,
s: rawStacktrace(s),
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: b.a,
s: rawStacktrace(s),
r: b.r,
attrs: b.attrs,
}
}
@@ -107,19 +116,21 @@ func Wrap(cause error, t typ, code Code, message string) *base {
// WithAdditionalf adds an additional error message to the existing error.
func WithAdditionalf(cause error, format string, args ...any) *base {
t, c, m, e, u, a := Unwrapb(cause)
t, c, m, e, u, a, r, attrs := Unwrapb(cause)
var s fmt.Stringer
if original, ok := cause.(*base); ok {
s = original.s
}
b := &base{
t: t,
c: c,
m: m,
e: e,
u: u,
a: a,
s: s,
t: t,
c: c,
m: m,
e: e,
u: u,
a: a,
s: s,
r: r,
attrs: attrs,
}
return b.WithAdditional(append(a, fmt.Sprintf(format, args...))...)
@@ -128,55 +139,197 @@ func WithAdditionalf(cause error, format string, args ...any) *base {
// WithUrl adds a url to the base error and returns a new base error.
func (b *base) WithUrl(u string) *base {
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: u,
a: b.a,
s: b.s,
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: u,
a: b.a,
s: b.s,
r: b.r,
attrs: b.attrs,
}
}
// WithAdditional adds additional messages to the base error and returns a new base error.
func (b *base) WithAdditional(a ...string) *base {
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: a,
s: b.s,
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: a,
s: b.s,
r: b.r,
attrs: b.attrs,
}
}
// WithRetry adds retry metadata to the base error and returns a new base error.
func (b *base) WithRetry(r retry) *base {
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: b.a,
s: b.s,
r: r,
attrs: b.attrs,
}
}
// setAttrs replaces the value at key with the given values and returns a new base error.
// Values are always stored as []string under a stable key.
// This helper is not exported; use WithSuggestions (or add a new With* method) instead.
func (b *base) setAttrs(key string, values ...string) *base {
copiedAttrs := make(map[string]any, len(b.attrs)+1)
maps.Copy(copiedAttrs, b.attrs)
replacement := make([]string, len(values))
copy(replacement, values)
copiedAttrs[key] = replacement
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: b.a,
s: b.s,
r: b.r,
attrs: copiedAttrs,
}
}
// addAttrs appends additional values to the existing list at key and returns a new base error.
// Values are always stored as []string under a stable key.
// This helper is not exported; use AddSuggestions, AddWarnings, or AddInvalidReferences,
// or add a new method that calls addAttrs for any specific attributes.
func (b *base) addAttrs(key string, values ...string) *base {
copiedAttrs := make(map[string]any, len(b.attrs)+1)
maps.Copy(copiedAttrs, b.attrs)
if len(values) > 0 {
existing, _ := copiedAttrs[key].([]string)
merged := make([]string, 0, len(existing)+len(values))
merged = append(merged, existing...)
merged = append(merged, values...)
copiedAttrs[key] = merged
}
return &base{
t: b.t,
c: b.c,
m: b.m,
e: b.e,
u: b.u,
a: b.a,
s: b.s,
r: b.r,
attrs: copiedAttrs,
}
}
// WithSuggestions replaces the list of suggestions on the base error.
func (b *base) WithSuggestions(suggestions ...string) *base {
return b.setAttrs("suggestions", suggestions...)
}
// AddSuggestions appends to the existing list of suggestions on the base error.
func (b *base) AddSuggestions(suggestions ...string) *base {
return b.addAttrs("suggestions", suggestions...)
}
// WithInvalidReference replaces the list of invalid references on the base error with a single entry.
func (b *base) WithInvalidReference(invalidReference string) *base {
return b.setAttrs("invalidReferences", invalidReference)
}
// WithInvalidReferences replaces the list of invalid references on the base error.
func (b *base) WithInvalidReferences(invalidReferences ...string) *base {
return b.setAttrs("invalidReferences", invalidReferences...)
}
// AddInvalidReference appends a single invalid reference to the existing list on the base error.
func (b *base) AddInvalidReference(invalidReference string) *base {
return b.addAttrs("invalidReferences", invalidReference)
}
// AddInvalidReferences appends to the existing list of invalid references on the base error.
func (b *base) AddInvalidReferences(invalidReferences ...string) *base {
return b.addAttrs("invalidReferences", invalidReferences...)
}
// WithWarnings replaces the list of warnings on the base error.
func (b *base) WithWarnings(warnings ...string) *base {
return b.setAttrs("warnings", warnings...)
}
// AddWarnings appends to the existing list of warnings on the base error.
func (b *base) AddWarnings(warnings ...string) *base {
return b.addAttrs("warnings", warnings...)
}
// WithRetryNever sets the retry policy to Never.
func (b *base) WithRetryNever() *base {
return b.WithRetry(retry{policy: RetryNever})
}
// WithRetryImmediate sets the retry policy to Immediate.
func (b *base) WithRetryImmediate() *base {
return b.WithRetry(retry{policy: RetryImmediate})
}
// WithRetryBackoff sets the retry policy to Backoff.
func (b *base) WithRetryBackoff() *base {
return b.WithRetry(retry{policy: RetryBackoff})
}
// WithRetryAfter sets the retry policy to After and requires a delay.
func (b *base) WithRetryAfter(delay time.Duration) *base {
return b.WithRetry(retry{policy: RetryAfter, after: delay})
}
// WithRetryAfterFix sets the retry policy to AfterFix.
func (b *base) WithRetryAfterFix() *base {
return b.WithRetry(retry{policy: RetryAfterFix})
}
// WithRetryAfterAuth sets the retry policy to AfterAuth.
func (b *base) WithRetryAfterAuth() *base {
return b.WithRetry(retry{policy: RetryAfterAuth})
}
// Unwrapb is a combination of built-in errors.As and type casting.
// It finds the first error in cause that matches base,
// and if one is found, returns the individual fields of base.
// Otherwise, it returns TypeInternal, the original error string
// and the error itself.
//
//nolint:staticcheck // ST1008: intentional return order matching struct field order (TCMEUA)
func Unwrapb(cause error) (typ, Code, string, error, string, []string) {
//nolint:staticcheck // ST1008: intentional return order matching struct field order (TCMEUARA)
func Unwrapb(cause error) (typ, Code, string, error, string, []string, retry, map[string]any) {
base, ok := cause.(*base)
if ok {
return base.t, base.c, base.m, base.e, base.u, base.a
return base.t, base.c, base.m, base.e, base.u, base.a, base.r, base.attrs
}
return TypeInternal, CodeUnknown, cause.Error(), cause, "", []string{}
return TypeInternal, CodeUnknown, cause.Error(), cause, "", []string{}, retry{policy: RetryNever}, map[string]any{}
}
// Ast checks if the provided error matches the specified custom error type.
func Ast(cause error, typ typ) bool {
t, _, _, _, _, _ := Unwrapb(cause)
t, _, _, _, _, _, _, _ := Unwrapb(cause)
return t == typ
}
// Asc checks if the provided error matches the specified custom error code.
func Asc(cause error, code Code) bool {
_, c, _, _, _, _ := Unwrapb(cause)
_, c, _, _, _, _, _, _ := Unwrapb(cause)
return c.s == code.s
}
@@ -198,57 +351,57 @@ func Is(err error, target error) bool {
// WrapNotFoundf is a wrapper around Wrapf with TypeNotFound.
func WrapNotFoundf(cause error, code Code, format string, args ...any) *base {
return Wrapf(cause, TypeNotFound, code, format, args...)
return Wrapf(cause, TypeNotFound, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// NewNotFoundf is a wrapper around Newf with TypeNotFound.
func NewNotFoundf(code Code, format string, args ...any) *base {
return Newf(TypeNotFound, code, format, args...)
return Newf(TypeNotFound, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// WrapInternalf is a wrapper around Wrapf with TypeInternal.
func WrapInternalf(cause error, code Code, format string, args ...any) *base {
return Wrapf(cause, TypeInternal, code, format, args...)
return Wrapf(cause, TypeInternal, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// NewInternalf is a wrapper around Newf with TypeInternal.
func NewInternalf(code Code, format string, args ...any) *base {
return Newf(TypeInternal, code, format, args...)
return Newf(TypeInternal, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// WrapInvalidInputf is a wrapper around Wrapf with TypeInvalidInput.
func WrapInvalidInputf(cause error, code Code, format string, args ...any) *base {
return Wrapf(cause, TypeInvalidInput, code, format, args...)
return Wrapf(cause, TypeInvalidInput, code, format, args...).WithRetry(retry{policy: RetryAfterFix})
}
// NewInvalidInputf is a wrapper around Newf with TypeInvalidInput.
func NewInvalidInputf(code Code, format string, args ...any) *base {
return Newf(TypeInvalidInput, code, format, args...)
}
// WrapUnexpectedf is a wrapper around Wrapf with TypeUnexpected.
func WrapUnexpectedf(cause error, code Code, format string, args ...any) *base {
return Wrapf(cause, TypeInvalidInput, code, format, args...)
}
// NewUnexpectedf is a wrapper around Newf with TypeUnexpected.
func NewUnexpectedf(code Code, format string, args ...any) *base {
return Newf(TypeInvalidInput, code, format, args...)
return Newf(TypeInvalidInput, code, format, args...).WithRetry(retry{policy: RetryAfterFix})
}
// NewMethodNotAllowedf is a wrapper around Newf with TypeMethodNotAllowed.
func NewMethodNotAllowedf(code Code, format string, args ...any) *base {
return Newf(TypeMethodNotAllowed, code, format, args...)
return Newf(TypeMethodNotAllowed, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// WrapTimeoutf is a wrapper around Wrapf with TypeTimeout.
func WrapTimeoutf(cause error, code Code, format string, args ...any) *base {
return Wrapf(cause, TypeTimeout, code, format, args...)
return Wrapf(cause, TypeTimeout, code, format, args...).WithRetry(retry{policy: RetryBackoff})
}
// NewTimeoutf is a wrapper around Newf with TypeTimeout.
func NewTimeoutf(code Code, format string, args ...any) *base {
return Newf(TypeTimeout, code, format, args...)
return Newf(TypeTimeout, code, format, args...).WithRetry(retry{policy: RetryBackoff})
}
// NewUnauthenticatedf is a wrapper around Newf with TypeUnauthenticated.
func NewUnauthenticatedf(code Code, format string, args ...any) *base {
return Newf(TypeUnauthenticated, code, format, args...).WithRetry(retry{policy: RetryAfterAuth})
}
// NewForbiddenf is a wrapper around Newf with TypeForbidden.
func NewForbiddenf(code Code, format string, args ...any) *base {
return Newf(TypeForbidden, code, format, args...).WithRetry(retry{policy: RetryNever})
}
// Attr returns an slog.Attr with a standardized "exception" key for the given error.
@@ -259,6 +412,13 @@ func Attr(err error) slog.Attr {
// TypeAttr returns an OTel attribute.KeyValue with the "error.type" semconv key
// set to the error's type string.
func TypeAttr(err error) attribute.KeyValue {
t, _, _, _, _, _ := Unwrapb(err)
t, _, _, _, _, _, _, _ := Unwrapb(err)
return attribute.String("error.type", t.String())
}
func RetryAfterOf(err error) (delay time.Duration, ok bool) {
_, _, _, _, _, _, r, _ := Unwrapb(err)
delay = r.after
return delay, r.policy == RetryAfter
}

View File

@@ -3,10 +3,30 @@ package errors
import (
"errors" //nolint:depguard
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestFormatDuration(t *testing.T) {
cases := []struct {
in time.Duration
want string
}{
{0, "0s"},
{1 * time.Second, "1s"},
{5 * time.Second, "5s"},
{1500 * time.Millisecond, "1.5s"},
{500 * time.Millisecond, "0.5s"},
{1*time.Second + 3*time.Nanosecond, "1.000000003s"},
{90 * time.Minute, "5400s"},
{-2 * time.Second, "-2s"},
}
for _, c := range cases {
assert.Equal(t, c.want, formatDuration(c.in), "input: %v", c.in)
}
}
func TestNew(t *testing.T) {
typ := typ{"test-error"}
err := New(typ, MustNewCode("code"), "test error info")
@@ -40,7 +60,7 @@ func TestUnwrapb(t *testing.T) {
oerr := errors.New("original error")
berr := Wrapf(oerr, typ, MustNewCode("test_code"), "this is a base err").WithUrl("https://docs").WithAdditional("additional err")
atyp, acode, amessage, aerr, au, aa := Unwrapb(berr)
atyp, acode, amessage, aerr, au, aa, _, _ := Unwrapb(berr)
assert.Equal(t, typ, atyp)
assert.Equal(t, "test_code", acode.String())
assert.Equal(t, "this is a base err", amessage)
@@ -48,7 +68,7 @@ func TestUnwrapb(t *testing.T) {
assert.Equal(t, "https://docs", au)
assert.Equal(t, []string{"additional err"}, aa)
atyp, _, _, _, _, _ = Unwrapb(oerr)
atyp, _, _, _, _, _, _, _ = Unwrapb(oerr)
assert.Equal(t, TypeInternal, atyp)
}
@@ -59,13 +79,90 @@ func TestAttr(t *testing.T) {
assert.Equal(t, err, attr.Value.Any())
}
func TestWithSuggestions(t *testing.T) {
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithSuggestions("try this")
_, _, _, _, _, _, _, attrs := Unwrapb(err)
assert.Equal(t, map[string]any{"suggestions": []string{"try this"}}, attrs)
// WithSuggestions replaces the existing list.
err = err.WithSuggestions("try this instead")
_, _, _, _, _, _, _, attrs = Unwrapb(err)
assert.Equal(t, []string{"try this instead"}, attrs["suggestions"])
// AddSuggestions appends to the existing list.
err = err.AddSuggestions("and also this")
_, _, _, _, _, _, _, attrs = Unwrapb(err)
assert.Equal(t, []string{"try this instead", "and also this"}, attrs["suggestions"])
}
func TestWithWarnings(t *testing.T) {
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithWarnings("warn one")
_, _, _, _, _, _, _, attrs := Unwrapb(err)
assert.Equal(t, map[string]any{"warnings": []string{"warn one"}}, attrs)
// WithWarnings replaces the existing list.
err = err.WithWarnings("warn two")
_, _, _, _, _, _, _, attrs = Unwrapb(err)
assert.Equal(t, []string{"warn two"}, attrs["warnings"])
// AddWarnings appends to the existing list.
err = err.AddWarnings("warn three")
_, _, _, _, _, _, _, attrs = Unwrapb(err)
assert.Equal(t, []string{"warn two", "warn three"}, attrs["warnings"])
}
func TestWithRetryNever(t *testing.T) {
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryNever()
_, _, _, _, _, _, r, _ := Unwrapb(err)
assert.Equal(t, RetryNever, r.policy)
}
func TestWithRetryImmediate(t *testing.T) {
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryImmediate()
_, _, _, _, _, _, r, _ := Unwrapb(err)
assert.Equal(t, RetryImmediate, r.policy)
}
func TestWithRetryBackoff(t *testing.T) {
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryBackoff()
_, _, _, _, _, _, r, _ := Unwrapb(err)
assert.Equal(t, RetryBackoff, r.policy)
}
func TestWithRetryAfter(t *testing.T) {
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryAfter(5 * time.Microsecond)
_, _, _, _, _, _, r, _ := Unwrapb(err)
assert.Equal(t, RetryAfter, r.policy)
assert.Equal(t, 5, int(r.after.Microseconds()))
}
func TestWithRetryAfterFix(t *testing.T) {
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryAfterFix()
_, _, _, _, _, _, r, _ := Unwrapb(err)
assert.Equal(t, RetryAfterFix, r.policy)
}
func TestWithRetryAfterAuth(t *testing.T) {
err := New(TypeInternal, MustNewCode("test_code"), "test error").WithRetryAfterAuth()
_, _, _, _, _, _, r, _ := Unwrapb(err)
assert.Equal(t, RetryAfterAuth, r.policy)
}
func TestWithStacktrace(t *testing.T) {
err := New(TypeInternal, MustNewCode("test_code"), "panic").WithStacktrace("custom stack trace")
assert.Equal(t, "custom stack trace", err.Stacktrace())
assert.Equal(t, "panic", err.Error())
typ, code, message, _, _, _ := Unwrapb(err)
typ, code, message, _, _, _, _, _ := Unwrapb(err)
assert.Equal(t, TypeInternal, typ)
assert.Equal(t, "test_code", code.String())
assert.Equal(t, "panic", message)

View File

@@ -2,40 +2,90 @@ package errors
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"time"
)
type JSON struct {
Type string `json:"type" required:"true"`
Code string `json:"code" required:"true"`
Message string `json:"message" required:"true"`
Url string `json:"url,omitempty"`
Errors []responseerroradditional `json:"errors,omitempty"`
Retry *responseretryjson `json:"retry,omitempty"`
Attrs responseattributes `json:"attrs,omitempty"`
}
type responseretryjson struct {
Policy responseretrypolicy `json:"policy"`
// After is the retry delay in google.protobuf.Duration JSON form ("5s", "1.500s").
// Present only when Policy == "after".
After string `json:"after,omitempty" format:"duration" example:"5.5s" description:"Retry delay in google.protobuf.Duration JSON form. Present only when policy is \"after\"."`
}
type responseretrypolicy string
func (r responseretrypolicy) String() string { return string(r) }
func (responseretrypolicy) Enum() []any {
return []any{
RetryNever,
RetryImmediate,
RetryBackoff,
RetryAfter,
RetryAfterFix,
RetryAfterAuth,
}
}
type responseerroradditional struct {
Message string `json:"message"`
}
type responseattributes map[string]any
func AsJSON(cause error) *JSON {
// See if this is an instance of the base error or not
_, c, m, _, u, a := Unwrapb(cause)
t, c, m, _, u, a, r, attrs := Unwrapb(cause)
rea := make([]responseerroradditional, len(a))
for k, v := range a {
rea[k] = responseerroradditional{v}
}
var retry *responseretryjson
if r.policy != "" {
retry = &responseretryjson{
Policy: responseretrypolicy(r.policy),
}
if r.policy == RetryAfter {
retry.After = formatDuration(r.after)
}
}
if len(attrs) == 0 {
attrs = nil
}
return &JSON{
Type: t.String(),
Code: c.String(),
Message: m,
Url: u,
Errors: rea,
Retry: retry,
Attrs: attrs,
}
}
// AsURLValues is only used for SSO, ideally this method shouldn't be here.
// TODO(pandey): Update the method or the comment.
func AsURLValues(cause error) url.Values {
// See if this is an instance of the base error or not
_, c, m, _, u, a := Unwrapb(cause)
t, c, m, _, u, a, _, _ := Unwrapb(cause)
rea := make([]responseerroradditional, len(a))
for k, v := range a {
@@ -45,6 +95,7 @@ func AsURLValues(cause error) url.Values {
errors, err := json.Marshal(rea)
if err != nil {
return url.Values{
"type": {t.String()},
"code": {c.String()},
"message": {m},
"url": {u},
@@ -52,9 +103,34 @@ func AsURLValues(cause error) url.Values {
}
return url.Values{
"type": {t.String()},
"code": {c.String()},
"message": {m},
"url": {u},
"errors": {string(errors)},
}
}
// formatDuration serializes a time.Duration as a google.protobuf.Duration JSON value
// (https://protobuf.dev/reference/protobuf/google.protobuf/#duration): "<seconds>[.<fractional>]s".
// Trailing zeros in the fractional part are stripped.
func formatDuration(d time.Duration) string {
if d == 0 {
return "0s"
}
negative := d < 0
if negative {
d = -d
}
sec := d / time.Second
nano := d % time.Second
out := strconv.FormatInt(int64(sec), 10)
if nano != 0 {
frac := strings.TrimRight(fmt.Sprintf("%09d", nano), "0")
out = out + "." + frac
}
if negative {
out = "-" + out
}
return out + "s"
}

19
pkg/errors/retry.go Normal file
View File

@@ -0,0 +1,19 @@
package errors
import (
"time"
)
const (
RetryNever string = "never" // retry with the same inputs cannot succeed.
RetryImmediate string = "immediate" // retry without waiting.
RetryBackoff string = "backoff" // caller picks its own backoff schedule.
RetryAfter string = "after" // honor Retry.After exactly (the producer knows the wait).
RetryAfterFix string = "after_fix" // retry pointless until the caller fixes the request.
RetryAfterAuth string = "after_auth" // retry pointless until the caller re-authenticates.
)
type retry struct {
policy string
after time.Duration
}

View File

@@ -1,20 +1,20 @@
package errors
var (
TypeInvalidInput typ = typ{"invalid-input"}
TypeInternal = typ{"internal"}
TypeUnsupported = typ{"unsupported"}
TypeNotFound = typ{"not-found"}
TypeMethodNotAllowed = typ{"method-not-allowed"}
TypeAlreadyExists = typ{"already-exists"}
TypeUnauthenticated = typ{"unauthenticated"}
TypeForbidden = typ{"forbidden"}
TypeCanceled = typ{"canceled"}
TypeTimeout = typ{"timeout"}
TypeUnexpected = typ{"unexpected"} // Generic mismatch of expectations
TypeFatal = typ{"fatal"} // Unrecoverable failure (e.g. panic)
TypeLicenseUnavailable = typ{"license-unavailable"}
TypeTooManyRequests = typ{"too-many-requests"}
TypeInvalidInput typ = typ{"invalid-input"}
TypeInternal = typ{"internal"}
TypeUnsupported = typ{"unsupported"}
TypeNotFound = typ{"not-found"}
TypeMethodNotAllowed = typ{"method-not-allowed"}
TypeAlreadyExists = typ{"already-exists"}
TypeUnauthenticated = typ{"unauthenticated"}
TypeForbidden = typ{"forbidden"}
TypeCanceled = typ{"canceled"}
TypeTimeout = typ{"timeout"}
// TypeUnexpected = typ{"unexpected"} // There's nothing unexpected in the world, just things we don't understand yet
TypeFatal = typ{"fatal"} // Unrecoverable failure (e.g. panic)
TypeLicenseUnavailable = typ{"license-unavailable"}
TypeTooManyRequests = typ{"too-many-requests"}
)
// Defines custom error types.

View File

@@ -58,7 +58,7 @@ func TestJSONBinding_BindBodyErrors(t *testing.T) {
err := JSON.BindBody(strings.NewReader(testCase.body), testCase.obj, testCase.opts...)
assert.Error(t, err)
typ, c, m, _, _, a := errors.Unwrapb(err)
typ, c, m, _, _, a, _, _ := errors.Unwrapb(err)
assert.Equal(t, errors.TypeInvalidInput, typ)
assert.Equal(t, testCase.code, c)
assert.Equal(t, testCase.message, m)

View File

@@ -101,7 +101,7 @@ func TestRecovery(t *testing.T) {
err := extractException(t, records[0])
require.NotNil(t, err)
typ, _, message, _, _, _ := errors.Unwrapb(err)
typ, _, message, _, _, _, _, _ := errors.Unwrapb(err)
assert.Equal(t, errors.TypeFatal, typ)
assert.Equal(t, tc.wantMessage, message)

View File

@@ -2,6 +2,8 @@ package render
import (
"net/http"
"strconv"
"time"
"github.com/SigNoz/signoz/pkg/errors"
jsoniter "github.com/json-iterator/go"
@@ -86,7 +88,7 @@ func ErrorTypeFromStatusCode(statusCode int) string {
func Error(rw http.ResponseWriter, cause error) {
// Derive the http code from the error type
t, _, _, _, _, _ := errors.Unwrapb(cause)
t, _, _, _, _, _, _, _ := errors.Unwrapb(cause)
httpCode := http.StatusInternalServerError
switch t {
@@ -121,6 +123,13 @@ func Error(rw http.ResponseWriter, cause error) {
return
}
// Retry-After carries the explicit delay declared via
// errors.WithRetryAfter. Set it before WriteHeader so headers go on the wire.
if d, ok := errors.RetryAfterOf(cause); ok {
seconds := max(int64((d+time.Second-1)/time.Second), 0)
rw.Header().Set("Retry-After", strconv.FormatInt(seconds, 10))
}
rw.WriteHeader(httpCode)
_, _ = rw.Write(body)
}

View File

@@ -97,13 +97,13 @@ func TestError(t *testing.T) {
name: "AlreadyExists",
statusCode: http.StatusConflict,
err: errors.New(errors.TypeAlreadyExists, errors.MustNewCode("already_exists"), "already exists").WithUrl("https://already_exists"),
expected: []byte(`{"status":"error","error":{"code":"already_exists","message":"already exists","url":"https://already_exists"}}`),
expected: []byte(`{"status":"error","error":{"type":"already-exists","code":"already_exists","message":"already exists","url":"https://already_exists"}}`),
},
"/unauthenticated": {
name: "Unauthenticated",
statusCode: http.StatusUnauthorized,
err: errors.New(errors.TypeUnauthenticated, errors.MustNewCode("not_allowed"), "not allowed").WithUrl("https://unauthenticated").WithAdditional("a1", "a2"),
expected: []byte(`{"status":"error","error":{"code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1"},{"message":"a2"}]}}`),
expected: []byte(`{"status":"error","error":{"type":"unauthenticated","code":"not_allowed","message":"not allowed","url":"https://unauthenticated","errors":[{"message":"a1"},{"message":"a2"}]}}`),
},
}

View File

@@ -32,7 +32,7 @@ func (h *exception) Wrap(next LogHandler) LogHandler {
return next.Handle(ctx, record)
}
t, c, m, _, _, _ := errors.Unwrapb(foundErr)
t, c, m, _, _, _, _, _ := errors.Unwrapb(foundErr)
newRecord.AddAttrs(
slog.String("exception.type", t.String()),

View File

@@ -202,15 +202,15 @@ func (handler *handler) exportRawDataJSONL(rowChan <-chan *qbtypes.RawRow, errCh
}
jsonBytes, err := json.Marshal(row.Data)
if err != nil {
return false, errors.NewUnexpectedf(errors.CodeInternal, "error marshaling JSON: %s", err)
return false, errors.NewInternalf(errors.CodeInternal, "error marshaling JSON: %s", err)
}
totalBytes += uint64(len(jsonBytes)) + 1
if _, err := writer.Write(jsonBytes); err != nil {
return false, errors.NewUnexpectedf(errors.CodeInternal, "error writing JSON: %s", err)
return false, errors.NewInternalf(errors.CodeInternal, "error writing JSON: %s", err)
}
if _, err := writer.Write([]byte("\n")); err != nil {
return false, errors.NewUnexpectedf(errors.CodeInternal, "error writing JSON newline: %s", err)
return false, errors.NewInternalf(errors.CodeInternal, "error writing JSON newline: %s", err)
}
if totalBytes > MaxExportBytesLimit {

View File

@@ -74,7 +74,7 @@ func (module *getter) ListDeprecatedUsersByOrgID(ctx context.Context, orgID valu
roleNames := userIDToRoleNames[user.ID]
if len(roleNames) == 0 {
return nil, errors.Newf(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found for user: %s", user.ID.String())
return nil, errors.Newf(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found for user: %s", user.ID.String())
}
role := authtypes.SigNozManagedRoleToExistingLegacyRole[roleNames[0]]
@@ -113,11 +113,11 @@ func (module *getter) GetDeprecatedUserByOrgIDAndID(ctx context.Context, orgID v
}
if len(userRoles) == 0 {
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
}
if userRoles[0].Role == nil {
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
}
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
@@ -141,11 +141,11 @@ func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.Deprecate
}
if len(userRoles) == 0 {
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeUserRolesNotFound, "no user roles entries found")
}
if userRoles[0].Role == nil {
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
}
role := authtypes.SigNozManagedRoleToExistingLegacyRole[userRoles[0].Role.Name]
@@ -211,7 +211,7 @@ func (module *getter) GetRolesByUserID(ctx context.Context, userID valuer.UUID)
for _, ur := range userRoles {
if ur.Role == nil {
return nil, errors.New(errors.TypeUnexpected, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
return nil, errors.New(errors.TypeInternal, authtypes.ErrCodeRoleNotFound, "role not found for user role entry")
}
}

View File

@@ -295,7 +295,7 @@ type ApiResponse struct {
// toApiError translates a pkg/errors typed error into the legacy
// model.ApiError to preserve the v1 JSON response shape.
func toApiError(err error) *model.ApiError {
t, _, _, _, _, _ := errors.Unwrapb(err)
t, _, _, _, _, _, _, _ := errors.Unwrapb(err)
var typ model.ErrorType
switch t {

View File

@@ -216,7 +216,7 @@ func getOperators(ops []pipelinetypes.PipelineOperator) ([]pipelinetypes.Pipelin
func processSeverityParser(operator *pipelinetypes.PipelineOperator) error {
if operator.Type != "severity_parser" {
return errors.NewUnexpectedf(CodeInvalidOperatorType, "operator type received %s", operator.Type)
return errors.NewInternalf(CodeInvalidOperatorType, "operator type received %s", operator.Type)
}
parseFromNotNilCheck, err := fieldNotNilCheck(operator.ParseFrom)
@@ -236,7 +236,7 @@ func processSeverityParser(operator *pipelinetypes.PipelineOperator) error {
// processJSONParser converts simple JSON parser operator into multiple operators for JSONMapping of default variables
func processJSONParser(parent *pipelinetypes.PipelineOperator) ([]pipelinetypes.PipelineOperator, error) {
if parent.Type != "json_parser" {
return nil, errors.NewUnexpectedf(CodeInvalidOperatorType, "operator type received %s", parent.Type)
return nil, errors.NewInternalf(CodeInvalidOperatorType, "operator type received %s", parent.Type)
}
parseFromNotNilCheck, err := fieldNotNilCheck(parent.ParseFrom)

View File

@@ -40,11 +40,11 @@ func runLogsAndTracesTests(t *testing.T, tests []logsAndTracesTestCase) {
if tt.wantErr {
require.Error(t, errLogs)
assert.ErrorContains(t, errLogs, tt.wantErrMsg)
_, _, _, _, _, additionalLogs := errors.Unwrapb(errLogs)
_, _, _, _, _, additionalLogs, _, _ := errors.Unwrapb(errLogs)
assert.Equal(t, tt.wantAdditional, additionalLogs)
require.Error(t, errTraces)
assert.ErrorContains(t, errTraces, tt.wantErrMsg)
_, _, _, _, _, additionalTraces := errors.Unwrapb(errTraces)
_, _, _, _, _, additionalTraces, _, _ := errors.Unwrapb(errTraces)
assert.Equal(t, tt.wantAdditional, additionalTraces)
} else {
require.NoError(t, errLogs)
@@ -1039,7 +1039,7 @@ func TestRewriteForMetrics(t *testing.T) {
if tt.wantErr {
require.Error(t, err)
assert.ErrorContains(t, err, tt.wantErrMsg)
_, _, _, _, _, additional := errors.Unwrapb(err)
_, _, _, _, _, additional, _, _ := errors.Unwrapb(err)
assert.Equal(t, tt.wantAdditional, additional)
} else {
require.NoError(t, err)

View File

@@ -2415,7 +2415,7 @@ func TestFilterExprLogs(t *testing.T) {
require.Equal(t, tc.expectedArgs, args)
} else {
require.Error(t, err, "Expected error for query: %s", tc.query)
_, _, _, _, _, a := errors.Unwrapb(err)
_, _, _, _, _, a, _, _ := errors.Unwrapb(err)
contains := false
for _, warn := range a {
if strings.Contains(warn, tc.expectedErrorContains) {
@@ -2536,7 +2536,7 @@ func TestFilterExprLogsConflictNegation(t *testing.T) {
require.Equal(t, tc.expectedArgs, args)
} else {
require.Error(t, err, "Expected error for query: %s", tc.query)
_, _, _, _, _, a := errors.Unwrapb(err)
_, _, _, _, _, a, _, _ := errors.Unwrapb(err)
contains := false
for _, warn := range a {
if strings.Contains(warn, tc.expectedErrorContains) {

View File

@@ -1025,7 +1025,7 @@ func TestStmtBuilderBodyField(t *testing.T) {
require.Contains(t, err.Error(), c.expectedErr.Error())
} else {
if err != nil {
_, _, _, _, _, add := errors.Unwrapb(err)
_, _, _, _, _, add, _, _ := errors.Unwrapb(err)
t.Logf("error additionals: %v", add)
}
require.NoError(t, err)
@@ -1124,7 +1124,7 @@ func TestStmtBuilderBodyFullTextSearch(t *testing.T) {
require.Contains(t, err.Error(), c.expectedErr.Error())
} else {
if err != nil {
_, _, _, _, _, add := errors.Unwrapb(err)
_, _, _, _, _, add, _, _ := errors.Unwrapb(err)
t.Logf("error additionals: %v", add)
}
require.NoError(t, err)

View File

@@ -29,7 +29,7 @@ func NewContextWithClaims(ctx context.Context, claims Claims) context.Context {
func ClaimsFromContext(ctx context.Context) (Claims, error) {
claims, ok := ctx.Value(claimsKey{}).(Claims)
if !ok {
return Claims{}, errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
return Claims{}, errors.NewUnauthenticatedf(errors.CodeUnauthenticated, "unauthenticated")
}
return claims, nil
@@ -42,7 +42,7 @@ func NewContextWithAccessToken(ctx context.Context, accessToken string) context.
func AccessTokenFromContext(ctx context.Context) (string, error) {
accessToken, ok := ctx.Value(accessTokenKey{}).(string)
if !ok {
return "", errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
return "", errors.NewUnauthenticatedf(errors.CodeUnauthenticated, "unauthenticated")
}
return accessToken, nil
@@ -55,7 +55,7 @@ func NewContextWithAPIKey(ctx context.Context, apiKey string) context.Context {
func APIKeyFromContext(ctx context.Context) (string, error) {
apiKey, ok := ctx.Value(apiKeyKey{}).(string)
if !ok {
return "", errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated")
return "", errors.NewUnauthenticatedf(errors.CodeUnauthenticated, "unauthenticated")
}
return apiKey, nil
@@ -77,7 +77,7 @@ func (c *Claims) IsSelfAccess(id string) error {
return nil
}
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "only the user/admin can access their own resource")
return errors.NewForbiddenf(errors.CodeForbidden, "only the user/admin can access their own resource")
}
func (c *Claims) IdentityID() string {

View File

@@ -117,7 +117,7 @@ func wrapUnmarshalError(err error, errorFormat string, args ...interface{}) erro
}
// If it's already one of our wrapped errors with additional context, return as-is
_, _, _, _, _, additionals := errors.Unwrapb(err)
_, _, _, _, _, additionals, _, _ := errors.Unwrapb(err)
if len(additionals) > 0 {
return err
}
@@ -144,7 +144,7 @@ func wrapValidationError(err error, contextIdentifier string, errorFormat string
}
// Extract the underlying error details
_, _, innerMsg, _, _, additionals := errors.Unwrapb(err)
_, _, innerMsg, _, _, additionals, _, _ := errors.Unwrapb(err)
// Create a new error with the provided format
newErr := errors.NewInvalidInputf(

View File

@@ -130,7 +130,7 @@ func TestQueryRangeRequest_UnmarshalJSON_ErrorMessages(t *testing.T) {
assert.Contains(t, err.Error(), tt.wantErrMsg)
// Check if it's an error from our package using Unwrapb
_, _, _, _, _, additionals := errors.Unwrapb(err)
_, _, _, _, _, additionals, _, _ := errors.Unwrapb(err)
// Check additional hints if we have any
if len(additionals) > 0 {