Skip to content

Commit 0885148

Browse files
authored
fix(cli): wrap gRPC errors with friendlyError to strip wire-level noise (#21)
Introduce friendlyError() in internal/cli/errors.go that extracts the human-readable description from gRPC status errors, returns "not found" for NotFound codes, and strips internal package-name prefixes ("workflow: ", "store: ") that bleed through to user-facing messages. Apply friendlyError() to all gRPC call sites: approve, audit, deny, exec, policy (list/get/apply/delete/eval), revoke, and status. Add errors_test.go covering non-gRPC pass-through, NotFound mapping, wire-wrapper stripping, prefix stripping, and chained-prefix stripping. Fixes #16
1 parent 700bedd commit 0885148

8 files changed

Lines changed: 121 additions & 12 deletions

File tree

internal/cli/approve.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func newApproveCmd() *cobra.Command {
3434
Comment: comment,
3535
})
3636
if err != nil {
37-
return fmt.Errorf("approve: %w", err)
37+
return fmt.Errorf("approve: %w", friendlyError(err))
3838
}
3939

4040
req := resp.GetRequest()
@@ -70,7 +70,7 @@ func newDenyCmd() *cobra.Command {
7070
Reason: reason,
7171
})
7272
if err != nil {
73-
return fmt.Errorf("deny: %w", err)
73+
return fmt.Errorf("deny: %w", friendlyError(err))
7474
}
7575

7676
req := resp.GetRequest()

internal/cli/audit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func newAuditCmd() *cobra.Command {
6767

6868
resp, err := c.Service().QueryAudit(ctx, queryInput)
6969
if err != nil {
70-
return fmt.Errorf("audit query: %w", err)
70+
return fmt.Errorf("audit query: %w", friendlyError(err))
7171
}
7272

7373
events := resp.GetEvents()

internal/cli/errors.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright © 2026 Yu Technology Group, LLC d/b/a jitsudo
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cli
5+
6+
import (
7+
"errors"
8+
"strings"
9+
10+
"google.golang.org/grpc/codes"
11+
"google.golang.org/grpc/status"
12+
)
13+
14+
// internalPrefixes are package-path-style prefixes that appear in server error
15+
// messages but are implementation details not useful to end users.
16+
var internalPrefixes = []string{
17+
"workflow: ",
18+
"store: ",
19+
}
20+
21+
// friendlyError translates a gRPC status error into a user-facing error by
22+
// stripping the wire-level "rpc error: code = X desc = " wrapper and removing
23+
// known internal package-name prefixes from the description.
24+
//
25+
// Non-gRPC errors are returned unchanged.
26+
func friendlyError(err error) error {
27+
st, ok := status.FromError(err)
28+
if !ok {
29+
return err
30+
}
31+
if st.Code() == codes.NotFound {
32+
return errors.New("not found")
33+
}
34+
msg := st.Message()
35+
// Strip any leading internal-package prefixes (e.g. "workflow: ", "store: ").
36+
for _, prefix := range internalPrefixes {
37+
msg = strings.TrimPrefix(msg, prefix)
38+
}
39+
return errors.New(msg)
40+
}

internal/cli/errors_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright © 2026 Yu Technology Group, LLC d/b/a jitsudo
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cli
5+
6+
import (
7+
"errors"
8+
"testing"
9+
10+
"google.golang.org/grpc/codes"
11+
"google.golang.org/grpc/status"
12+
)
13+
14+
func TestFriendlyError_NonGRPC(t *testing.T) {
15+
orig := errors.New("plain error")
16+
got := friendlyError(orig)
17+
if got != orig {
18+
t.Errorf("expected same error for non-gRPC error; got %v", got)
19+
}
20+
}
21+
22+
func TestFriendlyError_NotFound(t *testing.T) {
23+
err := status.Error(codes.NotFound, "request req_123 not found")
24+
got := friendlyError(err)
25+
if got.Error() != "not found" {
26+
t.Errorf("expected %q; got %q", "not found", got.Error())
27+
}
28+
}
29+
30+
func TestFriendlyError_StripGRPCWrapper(t *testing.T) {
31+
err := status.Error(codes.InvalidArgument, "duration must be positive")
32+
got := friendlyError(err)
33+
if got.Error() != "duration must be positive" {
34+
t.Errorf("expected raw message; got %q", got.Error())
35+
}
36+
}
37+
38+
func TestFriendlyError_StripWorkflowPrefix(t *testing.T) {
39+
err := status.Error(codes.Internal, "workflow: state machine transition denied")
40+
got := friendlyError(err)
41+
if got.Error() != "state machine transition denied" {
42+
t.Errorf("expected prefix stripped; got %q", got.Error())
43+
}
44+
}
45+
46+
func TestFriendlyError_StripStorePrefix(t *testing.T) {
47+
err := status.Error(codes.Internal, "store: database connection failed")
48+
got := friendlyError(err)
49+
if got.Error() != "database connection failed" {
50+
t.Errorf("expected prefix stripped; got %q", got.Error())
51+
}
52+
}
53+
54+
func TestFriendlyError_ChainedPrefixes(t *testing.T) {
55+
// Both known prefixes should be stripped when chained (e.g. wrapped errors).
56+
err := status.Error(codes.Internal, "workflow: store: nested prefix")
57+
got := friendlyError(err)
58+
if got.Error() != "nested prefix" {
59+
t.Errorf("expected both chained prefixes stripped; got %q", got.Error())
60+
}
61+
}
62+
63+
func TestFriendlyError_UnknownGRPCCode(t *testing.T) {
64+
err := status.Error(codes.PermissionDenied, "not authorized")
65+
got := friendlyError(err)
66+
if got.Error() != "not authorized" {
67+
t.Errorf("expected message for unknown code; got %q", got.Error())
68+
}
69+
}

internal/cli/exec.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ provider-specific credential variables (e.g., MOCK_ACCESS_KEY for the mock provi
4242
RequestId: requestID,
4343
})
4444
if err != nil {
45-
return fmt.Errorf("get credentials: %w", err)
45+
return fmt.Errorf("get credentials: %w", friendlyError(err))
4646
}
4747

4848
// Build environment: inherit parent env + inject credentials.

internal/cli/policy.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func newPolicyListCmd() *cobra.Command {
4747

4848
resp, err := c.Service().ListPolicies(ctx, &jitsudov1alpha1.ListPoliciesInput{})
4949
if err != nil {
50-
return fmt.Errorf("list policies: %w", err)
50+
return fmt.Errorf("list policies: %w", friendlyError(err))
5151
}
5252

5353
policies := resp.GetPolicies()
@@ -85,7 +85,7 @@ func newPolicyGetCmd() *cobra.Command {
8585

8686
resp, err := c.Service().GetPolicy(ctx, &jitsudov1alpha1.GetPolicyInput{Id: args[0]})
8787
if err != nil {
88-
return fmt.Errorf("get policy: %w", err)
88+
return fmt.Errorf("get policy: %w", friendlyError(err))
8989
}
9090

9191
p := resp.GetPolicy()
@@ -155,7 +155,7 @@ func newPolicyApplyCmd() *cobra.Command {
155155
Enabled: !disable,
156156
})
157157
if err != nil {
158-
return fmt.Errorf("apply policy: %w", err)
158+
return fmt.Errorf("apply policy: %w", friendlyError(err))
159159
}
160160

161161
p := resp.GetPolicy()
@@ -191,7 +191,7 @@ func newPolicyDeleteCmd() *cobra.Command {
191191

192192
_, err = c.Service().DeletePolicy(ctx, &jitsudov1alpha1.DeletePolicyInput{Id: args[0]})
193193
if err != nil {
194-
return fmt.Errorf("delete policy: %w", err)
194+
return fmt.Errorf("delete policy: %w", friendlyError(err))
195195
}
196196

197197
fmt.Fprintf(cmd.OutOrStdout(), "Policy %s deleted.\n", args[0])
@@ -237,7 +237,7 @@ making any state changes. The input must match the OPA input structure:
237237
Type: pt,
238238
})
239239
if err != nil {
240-
return fmt.Errorf("eval policy: %w", err)
240+
return fmt.Errorf("eval policy: %w", friendlyError(err))
241241
}
242242

243243
out := cmd.OutOrStdout()

internal/cli/revoke.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func newRevokeCmd() *cobra.Command {
3434
Reason: reason,
3535
})
3636
if err != nil {
37-
return fmt.Errorf("revoke: %w", err)
37+
return fmt.Errorf("revoke: %w", friendlyError(err))
3838
}
3939

4040
req := resp.GetRequest()

internal/cli/status.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func newStatusCmd() *cobra.Command {
4242
Id: args[0],
4343
})
4444
if err != nil {
45-
return fmt.Errorf("get request: %w", err)
45+
return fmt.Errorf("get request: %w", friendlyError(err))
4646
}
4747
printRequest(out, resp.GetRequest())
4848
return nil
@@ -54,7 +54,7 @@ func newStatusCmd() *cobra.Command {
5454
}
5555
resp, err := c.Service().ListRequests(ctx, filter)
5656
if err != nil {
57-
return fmt.Errorf("list requests: %w", err)
57+
return fmt.Errorf("list requests: %w", friendlyError(err))
5858
}
5959

6060
requests := resp.GetRequests()

0 commit comments

Comments
 (0)