Skip to content

Commit

Permalink
api: support errors extended information
Browse files Browse the repository at this point in the history
Since Tarantool 2.4.1, iproto error responses contain extended info with
backtrace [1]. After this patch, Error would contain ExtendedInfo field
(BoxError object), if it was provided. Error() handle now will print
extended info, if possible.

1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/#responses-for-errors

Part of #209
  • Loading branch information
DifferentialOrange committed Dec 1, 2022
1 parent 3afc90d commit a2d2d1d
Show file tree
Hide file tree
Showing 9 changed files with 599 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
### Added

- Support iproto feature discovery (#120).
- Support errors extended information (#209).

### Changed

Expand Down
186 changes: 186 additions & 0 deletions box_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package tarantool

import (
"bytes"
"fmt"
)

const (
keyErrorStack = 0x00
keyErrorType = 0x00
keyErrorFile = 0x01
keyErrorLine = 0x02
keyErrorMessage = 0x03
keyErrorErrno = 0x04
keyErrorErrcode = 0x05
keyErrorFields = 0x06
)

// BoxError is a type representing Tarantool `box.error` object: a single
// MP_ERROR_STACK object with a link to the previous stack error.
// See https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/error/
//
// Since 1.10.0
type BoxError struct {
// Type is error type that implies its source (for example, "ClientError").
Type string
// File is a source code file where the error was caught.
File string
// Line is a number of line in the source code file where the error was caught.
Line uint64
// Msg is the text of reason.
Msg string
// Errno is the ordinal number of the error.
Errno uint64
// Code is the number of the error as defined in `errcode.h`.
Code uint64
// Fields are additional fields depending on error type. For example, if
// type is "AccessDeniedError", then it will include "object_type",
// "object_name", "access_type".
Fields map[string]interface{}
// Prev is the previous error in stack.
Prev *BoxError
}

// Error converts a BoxError to a string.
func (e *BoxError) Error() string {
s := fmt.Sprintf("%s (%s, code 0x%x), see %s line %d",
e.Msg, e.Type, e.Code, e.File, e.Line)

if e.Prev != nil {
return fmt.Sprintf("%s: %s", s, e.Prev)
}

return s
}

// Depth computes the count of errors in stack, including the current one.
func (e *BoxError) Depth() int {
depth := int(0)

cur := e
for cur != nil {
cur = cur.Prev
depth++
}

return depth
}

func decodeBoxError(d *decoder) (*BoxError, error) {
var l, larr, l1, l2 int
var errorStack []BoxError
var err error

if l, err = d.DecodeMapLen(); err != nil {
return nil, err
}

for ; l > 0; l-- {
var cd int
if cd, err = d.DecodeInt(); err != nil {
return nil, err
}
switch cd {
case keyErrorStack:
if larr, err = d.DecodeArrayLen(); err != nil {
return nil, err
}

errorStack = make([]BoxError, larr)

for i := 0; i < larr; i++ {
if l1, err = d.DecodeMapLen(); err != nil {
return nil, err
}

for ; l1 > 0; l1-- {
var cd1 int
if cd1, err = d.DecodeInt(); err != nil {
return nil, err
}
switch cd1 {
case keyErrorType:
if errorStack[i].Type, err = d.DecodeString(); err != nil {
return nil, err
}
case keyErrorFile:
if errorStack[i].File, err = d.DecodeString(); err != nil {
return nil, err
}
case keyErrorLine:
if errorStack[i].Line, err = d.DecodeUint64(); err != nil {
return nil, err
}
case keyErrorMessage:
if errorStack[i].Msg, err = d.DecodeString(); err != nil {
return nil, err
}
case keyErrorErrno:
if errorStack[i].Errno, err = d.DecodeUint64(); err != nil {
return nil, err
}
case keyErrorErrcode:
if errorStack[i].Code, err = d.DecodeUint64(); err != nil {
return nil, err
}
case keyErrorFields:
var mapk string
var mapv interface{}

errorStack[i].Fields = make(map[string]interface{})

if l2, err = d.DecodeMapLen(); err != nil {
return nil, err
}
for ; l2 > 0; l2-- {
if mapk, err = d.DecodeString(); err != nil {
return nil, err
}
if mapv, err = d.DecodeInterface(); err != nil {
return nil, err
}
errorStack[i].Fields[mapk] = mapv
}
default:
if err = d.Skip(); err != nil {
return nil, err
}
}
}

if i > 0 {
errorStack[i-1].Prev = &errorStack[i]
}
}
default:
if err = d.Skip(); err != nil {
return nil, err
}
}
}

if len(errorStack) == 0 {
return nil, fmt.Errorf("msgpack: unexpected empty BoxError stack on decode")
}

return &errorStack[0], nil
}

// UnmarshalMsgpack deserializes a BoxError value from a MessagePack
// representation.
func (e *BoxError) UnmarshalMsgpack(b []byte) error {
if e == nil {
panic("cannot unmarshal to a nil pointer")
}

buf := bytes.NewBuffer(b)
dec := newDecoder(buf)

if val, err := decodeBoxError(dec); err != nil {
return err
} else {
*e = *val
return nil
}
}
200 changes: 200 additions & 0 deletions box_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package tarantool_test

import (
"regexp"
"testing"

"github.com/stretchr/testify/require"
. "github.com/tarantool/go-tarantool"
)

var samples = map[string]BoxError{
"SimpleError": {
Type: "ClientError",
File: "config.lua",
Line: uint64(202),
Msg: "Unknown error",
Errno: uint64(0),
Code: uint64(0),
},
"AccessDeniedError": {
Type: "AccessDeniedError",
File: "/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c",
Line: uint64(535),
Msg: "Execute access to function 'forbidden_function' is denied for user 'no_grants'",
Errno: uint64(0),
Code: uint64(42),
Fields: map[string]interface{}{
"object_type": "function",
"object_name": "forbidden_function",
"access_type": "Execute",
},
},
"ChainedError": {
Type: "ClientError",
File: "config.lua",
Line: uint64(205),
Msg: "Timeout exceeded",
Errno: uint64(0),
Code: uint64(78),
Prev: &BoxError{
Type: "ClientError",
File: "config.lua",
Line: uint64(202),
Msg: "Unknown error",
Errno: uint64(0),
Code: uint64(0),
},
},
}

var stringCases = map[string]struct {
e BoxError
s string
}{
"SimpleError": {
samples["SimpleError"],
"Unknown error (ClientError, code 0x0), see config.lua line 202",
},
"AccessDeniedError": {
samples["AccessDeniedError"],
"Execute access to function 'forbidden_function' is denied for user " +
"'no_grants' (AccessDeniedError, code 0x2a), see " +
"/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c line 535",
},
"ChainedError": {
samples["ChainedError"],
"Timeout exceeded (ClientError, code 0x4e), see config.lua line 205: " +
"Unknown error (ClientError, code 0x0), see config.lua line 202",
},
}

func TestBoxErrorStringRepr(t *testing.T) {
for name, testcase := range stringCases {
t.Run(name, func(t *testing.T) {
require.Equal(t, testcase.s, testcase.e.Error())
})
}
}

var mpDecodeSamples = map[string]struct {
b []byte
ok bool
err *regexp.Regexp
}{
"OuterMapInvalidLen": {
[]byte{0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`),
},
"OuterMapInvalidKey": {
[]byte{0x81, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding int64`),
},
"OuterMapExtraKey": {
[]byte{0x82, 0x00, 0x91, 0x81, 0x02, 0x01, 0x11, 0x00},
true,
regexp.MustCompile(``),
},
"OuterMapExtraInvalidKey": {
[]byte{0x81, 0x11, 0x81},
false,
regexp.MustCompile(`EOF`),
},
"ArrayInvalidLen": {
[]byte{0x81, 0x00, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding array length`),
},
"ArrayZeroLen": {
[]byte{0x81, 0x00, 0x90},
false,
regexp.MustCompile(`msgpack: unexpected empty BoxError stack on decode`),
},
"InnerMapInvalidLen": {
[]byte{0x81, 0x00, 0x91, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`),
},
"InnerMapInvalidKey": {
[]byte{0x81, 0x00, 0x91, 0x81, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding int64`),
},
"InnerMapInvalidErrorType": {
[]byte{0x81, 0x00, 0x91, 0x81, 0x00, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`),
},
"InnerMapInvalidErrorFile": {
[]byte{0x81, 0x00, 0x91, 0x81, 0x01, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`),
},
"InnerMapInvalidErrorLine": {
[]byte{0x81, 0x00, 0x91, 0x81, 0x02, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`),
},
"InnerMapInvalidErrorMessage": {
[]byte{0x81, 0x00, 0x91, 0x81, 0x03, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`),
},
"InnerMapInvalidErrorErrno": {
[]byte{0x81, 0x00, 0x91, 0x81, 0x04, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`),
},
"InnerMapInvalidErrorErrcode": {
[]byte{0x81, 0x00, 0x91, 0x81, 0x05, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`),
},
"InnerMapInvalidErrorFields": {
[]byte{0x81, 0x00, 0x91, 0x81, 0x06, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`),
},
"InnerMapInvalidErrorFieldsKey": {
[]byte{0x81, 0x00, 0x91, 0x81, 0x06, 0x81, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`),
},
"InnerMapInvalidErrorFieldsValue": {
[]byte{0x81, 0x00, 0x91, 0x81, 0x06, 0x81, 0xa3, 0x6b, 0x65, 0x79, 0xc1},
false,
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding interface{}`),
},
"InnerMapExtraKey": {
[]byte{0x81, 0x00, 0x91, 0x81, 0x21, 0x00},
true,
regexp.MustCompile(``),
},
"InnerMapExtraInvalidKey": {
[]byte{0x81, 0x00, 0x91, 0x81, 0x21, 0x81},
false,
regexp.MustCompile(`EOF`),
},
}

func TestMessagePackDecode(t *testing.T) {
for name, testcase := range mpDecodeSamples {
t.Run(name, func(t *testing.T) {
var val *BoxError = &BoxError{}
err := val.UnmarshalMsgpack(testcase.b)
if testcase.ok {
require.Nilf(t, err, "No errors on decode")
} else {
require.Regexp(t, testcase.err, err.Error())
}
})
}
}

func TestMessagePackUnmarshalToNil(t *testing.T) {
var val *BoxError = nil
require.PanicsWithValue(t, "cannot unmarshal to a nil pointer",
func() { val.UnmarshalMsgpack(mpDecodeSamples["InnerMapExtraKey"].b) })
}
Loading

0 comments on commit a2d2d1d

Please sign in to comment.