Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom schema errors #18

Merged
merged 26 commits into from
Mar 15, 2023
Merged

Custom schema errors #18

merged 26 commits into from
Mar 15, 2023

Conversation

VojtechVitek
Copy link
Contributor

@VojtechVitek VojtechVitek commented Mar 1, 2023

Introduction

1. Define your own custom schema errors in RIDL file, for example:

error   1 Unauthorized    "unauthorized"        HTTP 401
error   2 ExpiredToken    "expired token"       HTTP 401
error   3 InvalidToken    "invalid token"       HTTP 401
error   4 Deactivated     "account deactivated" HTTP 403
error   5 ConfirmAccount  "confirm your email"  HTTP 403
error   6 AccessDenied    "access denied"       HTTP 403
error   7 MissingArgument "missing argument"    HTTP 400
error   8 UnexpectedValue "unexpected value"    HTTP 400
error 100 RateLimited     "too many requests"   HTTP 429
error 101 DatabaseDown    "service outage"      HTTP 503
error 102 ElasticDown     "search is degraded"  HTTP 503
error 103 NotImplemented  "not implemented"     HTTP 501
error 200 UserNotFound    "user not found"
error 201 UserBusy        "user busy"
error 202 InvalidUsername "invalid username"
error 300 FileTooBig      "file is too big (max 1GB)"
error 301 FileInfected    "file is infected"
error 302 FileType        "unsupported file type"

Note: Unless specified, the default HTTP status for webrpc errors is HTTP 400.

2. Return your custom schema error from your RPC endpoint:

func (s *RPC) Endpoint(ctx context.Context) error {
	return proto.ErrRateLimited // returns HTTP 429
}

You can then assert the error from the client:

err := rpc.Endpoint(ctx)
if err != nil {
	if errors.Is(err, proto.ErrRateLimited) {
		// apply back-off strategy and try again
	}
	// handle other error types
}

3. Return your custom schema error along with the underlying cause:

func (s *RPC) Endpoint(ctx context.Context) error {
	return ErrorWithCause(proto.ErrRateLimited, fmt.Errorf("1000 req/min exceeded")) // returns HTTP 429
}

You can still assert the error type errors.Is(err, proto.ErrRateLimited) or get the underlying cause with errors.Unwrap(err).

3. Return a generic Go error from your RPC endpoint (HTTP 400):

func (s *RPC) Endpoint(ctx context.Context) error {
        if _, err := io.ReadAll(f); err != nil {
		return fmt.Errorf("failed to read file: %w", err) // returns HTTP 400
	}
}

Breaking API changes

1. Deprecates existing werbrpc error functions and sentinel errors:

  • proto.WrapError() // Deprecated.
  • proto.Errorf() // Deprecated.
  • proto.HTTPStatusFromErrorCode()
  • proto.IsErrorCode()
  • proto.ErrCanceled // Deprecated.
  • proto.ErrUnknown // Deprecated.
  • proto.ErrFail // Deprecated.
  • proto.ErrInvalidArgument // Deprecated.
  • proto.ErrDeadlineExceeded // Deprecated.
  • proto.ErrNotFound // Deprecated.
  • proto.ErrBadRoute // Deprecated.
  • proto.ErrAlreadyExists // Deprecated.
  • proto.ErrPermissionDenied // Deprecated.
  • proto.ErrUnauthenticated // Deprecated.
  • proto.ErrResourceExhausted // Deprecated.
  • proto.ErrFailedPrecondition // Deprecated.
  • proto.ErrAborted // Deprecated.
  • proto.ErrOutOfRange // Deprecated.
  • proto.ErrUnimplemented // Deprecated.
  • proto.ErrInternal // Deprecated.
  • proto.ErrUnavailable // Deprecated.
  • proto.ErrDataLoss // Deprecated.
  • proto.ErrNone // Deprecated.

Note: You can bring these back via -legacyErrors=true webrpc-gen flag. This will allow you to gradually migrate to schema errors, while keeping your existing codebase working.

Breaking changes in the generated error code:

- type ErrorPayload struct {
- 	Status int    `json:"status"`
- 	Code   string `json:"code"`
- 	Cause  string `json:"cause,omitempty"`
- 	Msg    string `json:"msg"`
- 	Error  string `json:"error"`
- }
- 
- type Error interface {
- 	// Code is of the valid error codes
- 	Code() ErrorCode
- 
- 	// Msg returns a human-readable, unstructured messages describing the error
- 	Msg() string
- 
- 	// Cause is reason for the error
- 	Cause() error
- 
- 	// Error returns a string of the form "webrpc error <Code>: <Msg>"
- 	Error() string
- 
- 	// Error response payload
- 	Payload() ErrorPayload
- }
-
- type ErrorCode string
 
+ type WebRPCError struct {
+ 	Name       string `json:"error"`
+ 	Code       int    `json:"code"`
+ 	Message    string `json:"msg"`
+ 	Cause      string `json:"cause,omitempty"`
+ 	HTTPStatus int    `json:"status"`
+ 	cause      error
+ }

WebRPCError implements Go's error interface, carries HTTP status code and has a first-class support for (un)wrapping introduced in Go 1.13:

  • errors.Is(err, proto.RateLimited)
  • cause := errors.Unwrap(err)
  • etc.
- statusCode := proto.HTTPStatusFromErrorCode(err)
 
+ if rpcErr, ok := err.(proto.WebRPCError); ok {
+        statusCode = rpcErr.HTTPStatus
+ }

Migrate gradually with -legacyErrors=true flag

If you enable -legacyErrors=true flag on the webrpc-gen golang template, it will generate backward-compatible code for the deprecated Errorf() and WrapError() functions and predefined Err* error codes. All this code will be marked as deprecated, which should help you migrate to the ErrorWithCause() function once you start creating custom errors in your RIDL schema.

func (s *RPC) Endpoint(ctx context.Context) error {
	return Errorf(proto.ErrInvalidArgument, "userId is required") // still works under -legacyErrors=true
}
func (s *RPC) Endpoint(ctx context.Context) error {
	return WrapError(proto.ErrUnavailable, io.ErrUnexpectedEOF, "service unavailable") // still works under -legacyErrors=true
}

@VojtechVitek VojtechVitek changed the title WIP: Custom schema errors Custom schema errors Mar 3, 2023
@VojtechVitek VojtechVitek merged commit 4774aa2 into master Mar 15, 2023
@VojtechVitek VojtechVitek deleted the errors branch March 15, 2023 00:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant