diff --git a/pkg/errgrpc/grpc.go b/pkg/errgrpc/grpc.go index 5957759..fcf8734 100644 --- a/pkg/errgrpc/grpc.go +++ b/pkg/errgrpc/grpc.go @@ -222,7 +222,17 @@ func ToNative(err error) error { code = s.Code() } else { desc = err.Error() - code = codes.Unknown + // No gRPC status: default to Unknown, but map native context + // sentinels to their code so the switch below restores them + // instead of reclassifying as errdefs.ErrUnknown. + switch { + case errors.Is(err, context.DeadlineExceeded): + code = codes.DeadlineExceeded + case errors.Is(err, context.Canceled): + code = codes.Canceled + default: + code = codes.Unknown + } } var cls error // divide these into error classes, becomes the cause diff --git a/pkg/errgrpc/grpc_test.go b/pkg/errgrpc/grpc_test.go index 9427e68..c7ebee1 100644 --- a/pkg/errgrpc/grpc_test.go +++ b/pkg/errgrpc/grpc_test.go @@ -274,3 +274,72 @@ func TestGRPCNestedError(t *testing.T) { checkError(ToNative(ToGRPC(werr))) } + + +// TestToNativeContextSentinels verifies that ToNative preserves the identity +// of native context sentinels (context.DeadlineExceeded and context.Canceled) +// when they are passed directly, without first being mapped to a gRPC status +// error via ToGRPC. This is the path taken when a ttrpc/gRPC client returns +// ctx.Err() on a per-call deadline and the caller runs it through ToNative. +// +// Unlike TestGRPCRoundTrip, these cases do NOT go through ToGRPC first, so +// status.FromError reports codes.Unknown. Without preserving the sentinel, +// the result wraps errdefs.ErrUnknown and errdefs.IsDeadlineExceeded / +// errdefs.IsCanceled stop recognizing it. +func TestToNativeContextSentinels(t *testing.T) { + for _, testcase := range []struct { + name string + input error + cause error + is func(error) bool + str string + }{ + { + name: "bare deadline exceeded", + input: context.DeadlineExceeded, + cause: context.DeadlineExceeded, + is: errdefs.IsDeadlineExceeded, + str: "context deadline exceeded", + }, + { + name: "wrapped deadline exceeded", + input: fmt.Errorf("get state for abc: %w", context.DeadlineExceeded), + cause: context.DeadlineExceeded, + is: errdefs.IsDeadlineExceeded, + str: "get state for abc: context deadline exceeded", + }, + { + name: "bare canceled", + input: context.Canceled, + cause: context.Canceled, + is: errdefs.IsCanceled, + str: "context canceled", + }, + { + name: "wrapped canceled", + input: fmt.Errorf("operation aborted: %w", context.Canceled), + cause: context.Canceled, + is: errdefs.IsCanceled, + str: "operation aborted: context canceled", + }, + } { + t.Run(testcase.name, func(t *testing.T) { + nerr := ToNative(testcase.input) + t.Logf("input: %v, recovered: %v", testcase.input, nerr) + + if !errors.Is(nerr, testcase.cause) { + t.Fatalf("unexpected cause: !errors.Is(%v, %v)", nerr, testcase.cause) + } + if !testcase.is(nerr) { + t.Fatalf("errdefs Is helper did not match recovered error: %v", nerr) + } + // The sentinel must not be reclassified as an unknown error. + if errdefs.IsUnknown(nerr) { + t.Fatalf("recovered error misclassified as unknown: %v", nerr) + } + if nerr.Error() != testcase.str { + t.Fatalf("unexpected string: %q != %q", nerr.Error(), testcase.str) + } + }) + } +} diff --git a/pkg/go.sum b/pkg/go.sum index 23d8a5e..7893499 100644 --- a/pkg/go.sum +++ b/pkg/go.sum @@ -1,5 +1,5 @@ -github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= -github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=