forked from fl00r/go-tarantool-1.6
-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
api: support errors extended information
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
1 parent
3afc90d
commit 0dc61a7
Showing
9 changed files
with
599 additions
and
5 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,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 | ||
} | ||
} |
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,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) }) | ||
} |
Oops, something went wrong.