Skip to content

Commit

Permalink
feat: add package to handle end-user error messages.
Browse files Browse the repository at this point in the history
Because

- Connector errors produce an unfriendly output in VDP.
- In order to trigger a pipeline, several repositories are involved in
  the execution.

This commit

- Implements a way to add and extract end-user messages to errors.
  • Loading branch information
jvallesm committed Dec 15, 2023
1 parent 3236165 commit ca383a1
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 0 deletions.
50 changes: 50 additions & 0 deletions errmsg/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# errmsg

Add end-user messages to errors.

`err.Error()` doesn't usually provide a human-friendly output. `errmsg` allows
errors to carry an (extendable) end-user message that can be used in e.g.
handlers.

Here is an example on how it can be used:

```go
package connector

import (
// ...
"github.com/instill-ai/x/errmsg"
)

func (c *Client) sendReq(reqURL, method, contentType string, data io.Reader) ([]byte, error) {
// ...

res, err := c.HTTPClient.Do(req)
if err != nil {
err := fmt.Errorf("failed to call connector vendor: %w", err)
return nil, errmsg.AddMessage(err, "Failed to call Vendor API.")
}

if res.StatusCode < 200 || res.StatusCode >= 300 {
err := fmt.Errorf("vendor responded with status code %d", res.StatusCode)
msg := fmt.Sprintf("Vendor responded with a %d status code.", res.StatusCode)
return nil, errmsg.AddMessage(err, msg)
}

// ...
}
```

```go
package handler

func (h *PublicHandler) DoAction(ctx context.Context, req *pb.DoActionRequest) (*pb.DoActionResponse, error) {
resp, err := h.triggerActionSteps(ctx, req)
if err != nil {
resp.Outputs, resp.Metadata, err = h.triggerNamespacePipeline(ctx, req)
return nil, status.Error(asGRPCStatus(err), errmsg.MessageOrErr(err))
}

return resp, nil
}
```
67 changes: 67 additions & 0 deletions errmsg/errmsg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package errmsg

import (
"errors"
"fmt"
)

// endUserError is an error that holds an end-user message.
type endUserError struct {
message string
cause error
}

// Error implements the error interface by returning the internal error message.
func (e *endUserError) Error() string { return e.cause.Error() }

// Unwrap implements the Unwrap interface.
func (e *endUserError) Unwrap() error { return e.cause }

// As implements the required function to ensure errors.As can properly match
// endUserEror targets.
func (e *endUserError) As(target any) bool {
if tgt, ok := target.(**endUserError); ok {
*tgt = e
return true
}

return false
}

// AddMessage adds an end-user message to an error, prepending it to any
// potential existing message.
func AddMessage(err error, msg string) error {
if msgInCause := Message(err); msgInCause != "" {
msg = fmt.Sprintf("%s %s", msg, msgInCause)
}

return &endUserError{
cause: err,
message: msg,
}
}

// Message extracts an end-user message from the error.
func Message(err error) string {
for err != nil {
eu := new(endUserError)
if errors.As(err, &eu) && eu.message != "" {
return eu.message
}

err = errors.Unwrap(err)
}

return ""
}

// MessageOrErr extracts an end-user message from the error. If no message is
// found, err.Error() is returned.
func MessageOrErr(err error) string {
msg := Message(err)
if msg == "" {
return err.Error()
}

return msg
}
82 changes: 82 additions & 0 deletions errmsg/errmsg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package errmsg

import (
"errors"
"fmt"
"testing"

qt "github.com/frankban/quicktest"
pkgerrors "github.com/pkg/errors"
)

func TestAddAndExtractMessage(t *testing.T) {
c := qt.New(t)

testcases := []struct {
name string
wantMsg string
wantErr string
err error
}{
{
name: "no message",
wantMsg: "boom",
wantErr: "boom",
err: errors.New("boom"),
},
{
name: "message on top of stack",
wantMsg: "Something went wrong.",
wantErr: "boom",
err: AddMessage(errors.New("boom"), "Something went wrong."),
},
{
name: "message in wrapped error (fmt)",
wantMsg: "Something went wrong.",
wantErr: "bang: boom",
err: fmt.Errorf(
"bang: %w",
AddMessage(errors.New("boom"), "Something went wrong."),
),
},
{
name: "message in wrapped error (pkgerrors.Wrap)",
wantMsg: "Something went wrong.",
wantErr: "bang: boom",
err: pkgerrors.Wrap(
AddMessage(errors.New("boom"), "Something went wrong."),
"bang",
),
},
{
name: "message in joint error",
wantMsg: "Something went wrong.",
wantErr: "bang\nboom",
err: errors.Join(
errors.New("bang"),
AddMessage(errors.New("boom"), "Something went wrong."),
),
},
{
name: "multi-message error",
wantMsg: "An error happened. Something went wrong.",
wantErr: "bang: boom",
err: AddMessage(
// handle error coming from downstream
fmt.Errorf("bang: %w",
// downstream error also contains message
AddMessage(errors.New("boom"), "Something went wrong."),
),
// add message to downstream error
"An error happened.",
),
},
}

for _, tc := range testcases {
c.Run(tc.name, func(c *qt.C) {
c.Check(MessageOrErr(tc.err), qt.Equals, tc.wantMsg)
c.Check(tc.err, qt.ErrorMatches, tc.wantErr)
})
}
}

0 comments on commit ca383a1

Please sign in to comment.