-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add package to handle end-user error messages. (#13)
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
Showing
6 changed files
with
206 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package errmsg | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"testing" | ||
|
||
qt "github.com/frankban/quicktest" | ||
) | ||
|
||
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 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) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters