Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions statusx/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,19 @@ func WrapCode(err error, code codes.Code, message string) *Status {
func WrapCodef(err error, code codes.Code, format string, a ...any) *Status {
return Wrapf(err, code, ReasonFromCode(code).String(), format, a...)
}

// AlwaysWrapCode is like WrapCode but always sets the given code and message,
// even if err is already a Status error. The original error is preserved as the cause.
// If err is nil, it returns an OK status. See AlwaysWrap for details on the
// codes.OK + non-nil error edge case.
func AlwaysWrapCode(err error, code codes.Code, message string) *Status {
return AlwaysWrap(err, code, ReasonFromCode(code).String(), message)
}

// AlwaysWrapCodef is like WrapCodef but always sets the given code and formatted message,
// even if err is already a Status error. The original error is preserved as the cause.
// If err is nil, it returns an OK status. See AlwaysWrap for details on the
// codes.OK + non-nil error edge case.
func AlwaysWrapCodef(err error, code codes.Code, format string, a ...any) *Status {
return AlwaysWrapf(err, code, ReasonFromCode(code).String(), format, a...)
}
32 changes: 32 additions & 0 deletions statusx/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,3 +458,35 @@ func Wrap(err error, c codes.Code, reason, message string) *Status {
func Wrapf(err error, c codes.Code, reason, format string, a ...any) *Status {
return Wrap(err, c, reason, fmt.Sprintf(format, a...))
}

// AlwaysWrap is like Wrap but always sets the underlying code, reason, and message
// fields, even if err is already a Status error. The original error is preserved as
// the cause. If err is nil, it returns an OK status (consistent with Wrap).
//
// Note: the computed Code()/Reason() follow the Status invariant that a non-nil cause
// cannot be OK. If err is non-nil and c == codes.OK, Code() returns codes.Unknown and
// Reason() returns "UNKNOWN", even though the stored fields are set as given.
//
// When wrapping an existing Status, structural details (field violations, metadata, etc.)
// are preserved via Clone, but the localized key is reset to match the new reason.
// Use WithLocalized/WithLocalizedArgs on the result if custom localization is needed.
func AlwaysWrap(err error, c codes.Code, reason, message string) *Status {
if err == nil {
return New(codes.OK, statusv1.ErrorReason_OK.String(), "")
}
s, ok := FromError(err)
if ok {
s = Clone(s)
Comment thread
molon marked this conversation as resolved.
}
s.cause = errors.WithStack(err)
s.code = c
s.message = message
s.errorInfo.Reason = reason
// Immediately fix key to creation-time reason
s.localized = &statusv1.Localized{Key: s.Reason()}
Comment thread
molon marked this conversation as resolved.
return s
}

func AlwaysWrapf(err error, c codes.Code, reason, format string, a ...any) *Status {
return AlwaysWrap(err, c, reason, fmt.Sprintf(format, a...))
}
154 changes: 152 additions & 2 deletions statusx/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,11 @@ func TestWrap(t *testing.T) {
}

{
status, _ := status.New(codes.NotFound, "resource not found").WithDetails(&errdetails.ErrorInfo{
st, err := status.New(codes.NotFound, "resource not found").WithDetails(&errdetails.ErrorInfo{
Reason: "NOT_FOUND",
})
wrapped := Wrap(status.Err(), codes.Internal, statusv1.ErrorReason_INTERNAL.String(), "internal server error")
require.NoError(t, err)
wrapped := Wrap(st.Err(), codes.Internal, statusv1.ErrorReason_INTERNAL.String(), "internal server error")
assert.Equal(t, codes.NotFound, wrapped.Code())
assert.Equal(t, "NOT_FOUND", wrapped.Reason())
assert.Equal(t, "resource not found", wrapped.Message())
Expand All @@ -287,6 +288,155 @@ func TestWrap(t *testing.T) {
})
}

func TestAlwaysWrap(t *testing.T) {
t.Run("plain error", func(t *testing.T) {
originalErr := errors.New("original error")
wrapped := AlwaysWrap(originalErr, codes.Internal, "INTERNAL_ERROR", "internal server error")
require.NotNil(t, wrapped)

assert.Equal(t, codes.Internal, wrapped.Code())
assert.Equal(t, "INTERNAL_ERROR", wrapped.Reason())
assert.Equal(t, "internal server error", wrapped.Message())
assert.True(t, errors.Is(wrapped.Err(), originalErr))
})

t.Run("nil error", func(t *testing.T) {
wrapped := AlwaysWrap(nil, codes.Internal, "INTERNAL_ERROR", "internal server error")
require.NotNil(t, wrapped)
assert.Equal(t, codes.OK, wrapped.Code())
assert.Equal(t, statusv1.ErrorReason_OK.String(), wrapped.Reason())
assert.Equal(t, "", wrapped.Message())
})

Comment thread
molon marked this conversation as resolved.
t.Run("overrides existing StatusError", func(t *testing.T) {
original := New(codes.NotFound, "NOT_FOUND", "resource not found")
wrapped := AlwaysWrap(original.Err(), codes.Internal, "INTERNAL_ERROR", "internal server error")
require.NotNil(t, wrapped)

assert.Equal(t, codes.Internal, wrapped.Code())
assert.Equal(t, "INTERNAL_ERROR", wrapped.Reason())
assert.Equal(t, "internal server error", wrapped.Message())
})

t.Run("overrides existing gRPC status error", func(t *testing.T) {
st, err := status.New(codes.NotFound, "resource not found").WithDetails(&errdetails.ErrorInfo{
Reason: "NOT_FOUND",
})
require.NoError(t, err)
wrapped := AlwaysWrap(st.Err(), codes.Internal, "INTERNAL_ERROR", "internal server error")
require.NotNil(t, wrapped)

assert.Equal(t, codes.Internal, wrapped.Code())
assert.Equal(t, "INTERNAL_ERROR", wrapped.Reason())
assert.Equal(t, "internal server error", wrapped.Message())
})

t.Run("preserves details from existing StatusError", func(t *testing.T) {
original := New(codes.InvalidArgument, "VALIDATION_FAILED", "validation failed").
WithFieldViolations(
NewFieldViolation("email", "field.email.invalid", "Email is invalid"),
).
WithMetadata(map[string]string{"key": "value"})

wrapped := AlwaysWrap(original.Err(), codes.Internal, "INTERNAL_ERROR", "internal server error")
require.NotNil(t, wrapped)

assert.Equal(t, codes.Internal, wrapped.Code())
assert.Equal(t, "INTERNAL_ERROR", wrapped.Reason())
assert.Equal(t, "internal server error", wrapped.Message())
assert.NotNil(t, wrapped.badRequest)
assert.Len(t, wrapped.badRequest.FieldViolations, 1)
assert.Equal(t, "email", wrapped.badRequest.FieldViolations[0].Field)
})

t.Run("does not mutate original StatusError", func(t *testing.T) {
original := New(codes.NotFound, "NOT_FOUND", "resource not found").
WithMetadata(map[string]string{"key": "value"})

_ = AlwaysWrap(original.Err(), codes.Internal, "INTERNAL_ERROR", "internal server error")

assert.Equal(t, codes.NotFound, original.Code())
assert.Equal(t, "NOT_FOUND", original.Reason())
assert.Equal(t, "resource not found", original.Message())
})

t.Run("localized key matches new reason", func(t *testing.T) {
original := New(codes.NotFound, "NOT_FOUND", "resource not found")
wrapped := AlwaysWrap(original.Err(), codes.Internal, "DATABASE_ERROR", "database failed")
require.NotNil(t, wrapped)

localized := wrapped.Localized()
require.NotNil(t, localized)
assert.Equal(t, "DATABASE_ERROR", localized.Key)
})
}

func TestAlwaysWrapf(t *testing.T) {
t.Run("plain error with format", func(t *testing.T) {
originalErr := errors.New("original error")
wrapped := AlwaysWrapf(originalErr, codes.Internal, "INTERNAL_ERROR", "error for %s: %d", "user", 42)
require.NotNil(t, wrapped)

assert.Equal(t, codes.Internal, wrapped.Code())
assert.Equal(t, "INTERNAL_ERROR", wrapped.Reason())
assert.Equal(t, "error for user: 42", wrapped.Message())
})

t.Run("overrides existing StatusError with format", func(t *testing.T) {
original := New(codes.NotFound, "NOT_FOUND", "resource not found")
wrapped := AlwaysWrapf(original.Err(), codes.Internal, "INTERNAL_ERROR", "error for %s", "user")
require.NotNil(t, wrapped)

assert.Equal(t, codes.Internal, wrapped.Code())
assert.Equal(t, "INTERNAL_ERROR", wrapped.Reason())
assert.Equal(t, "error for user", wrapped.Message())
})
}

func TestAlwaysWrapCode(t *testing.T) {
t.Run("plain error", func(t *testing.T) {
originalErr := errors.New("original error")
wrapped := AlwaysWrapCode(originalErr, codes.Internal, "internal server error")
require.NotNil(t, wrapped)

assert.Equal(t, codes.Internal, wrapped.Code())
assert.Equal(t, statusv1.ErrorReason_INTERNAL.String(), wrapped.Reason())
assert.Equal(t, "internal server error", wrapped.Message())
})

t.Run("overrides existing StatusError", func(t *testing.T) {
original := New(codes.NotFound, "NOT_FOUND", "resource not found")
wrapped := AlwaysWrapCode(original.Err(), codes.Internal, "internal server error")
require.NotNil(t, wrapped)

assert.Equal(t, codes.Internal, wrapped.Code())
assert.Equal(t, statusv1.ErrorReason_INTERNAL.String(), wrapped.Reason())
assert.Equal(t, "internal server error", wrapped.Message())
})
}

func TestAlwaysWrapCodef(t *testing.T) {
t.Run("plain error with format", func(t *testing.T) {
originalErr := errors.New("original error")
wrapped := AlwaysWrapCodef(originalErr, codes.Internal, "error for %s", "user")
require.NotNil(t, wrapped)

assert.Equal(t, codes.Internal, wrapped.Code())
assert.Equal(t, statusv1.ErrorReason_INTERNAL.String(), wrapped.Reason())
assert.Equal(t, "error for user", wrapped.Message())
})

t.Run("overrides existing StatusError", func(t *testing.T) {
original := New(codes.NotFound, "NOT_FOUND", "resource not found")
wrapped := AlwaysWrapCodef(original.Err(), codes.Internal, "error for %s", "user")
require.NotNil(t, wrapped)

assert.Equal(t, codes.Internal, wrapped.Code())
assert.Equal(t, statusv1.ErrorReason_INTERNAL.String(), wrapped.Reason())
assert.Equal(t, "error for user", wrapped.Message())
})
}

func TestGRPCStatus(t *testing.T) {
s := New(codes.PermissionDenied, statusv1.ErrorReason_PERMISSION_DENIED.String(), "permission denied").
WithLocalized("error.permission_denied", "access", "user").
Expand Down
Loading