Skip to content

Commit 1343297

Browse files
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
1 parent 594e5a9 commit 1343297

9 files changed

+599
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1111
### Added
1212

1313
- Support iproto feature discovery (#120)
14+
- Support errors extended information (#209)
1415

1516
### Changed
1617

box_error.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package tarantool
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
)
7+
8+
const (
9+
keyErrorStack = 0x00
10+
keyErrorType = 0x00
11+
keyErrorFile = 0x01
12+
keyErrorLine = 0x02
13+
keyErrorMessage = 0x03
14+
keyErrorErrno = 0x04
15+
keyErrorErrcode = 0x05
16+
keyErrorFields = 0x06
17+
)
18+
19+
// BoxError is a type representing Tarantool `box.error` object: a single
20+
// MP_ERROR_STACK object with a link to the previous stack error.
21+
// See https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/error/
22+
//
23+
// Since 1.10.0
24+
type BoxError struct {
25+
// Type is error type that implies its source (for example, "ClientError").
26+
Type string
27+
// File is a source code file where the error was caught.
28+
File string
29+
// Line is a number of line in the source code file where the error was caught.
30+
Line uint64
31+
// Msg is the text of reason.
32+
Msg string
33+
// Errno is the ordinal number of the error.
34+
Errno uint64
35+
// Code is the number of the error as defined in `errcode.h`.
36+
Code uint64
37+
// Fields are additional fields depending on error type. For example, if
38+
// type is "AccessDeniedError", then it will include "object_type",
39+
// "object_name", "access_type".
40+
Fields map[string]interface{}
41+
// Prev is the previous error in stack.
42+
Prev *BoxError
43+
}
44+
45+
// Error converts a BoxError to a string.
46+
func (e *BoxError) Error() string {
47+
s := fmt.Sprintf("%s (%s, code 0x%x), see %s line %d",
48+
e.Msg, e.Type, e.Code, e.File, e.Line)
49+
50+
if e.Prev != nil {
51+
return fmt.Sprintf("%s: %s", s, e.Prev)
52+
}
53+
54+
return s
55+
}
56+
57+
// Depth computes the count of errors in stack, including the current one.
58+
func (e *BoxError) Depth() int {
59+
depth := int(0)
60+
61+
cur := e
62+
for cur != nil {
63+
cur = cur.Prev
64+
depth++
65+
}
66+
67+
return depth
68+
}
69+
70+
func decodeBoxError(d *decoder) (*BoxError, error) {
71+
var l, larr, l1, l2 int
72+
var errorStack []BoxError
73+
var err error
74+
75+
if l, err = d.DecodeMapLen(); err != nil {
76+
return nil, err
77+
}
78+
79+
for ; l > 0; l-- {
80+
var cd int
81+
if cd, err = d.DecodeInt(); err != nil {
82+
return nil, err
83+
}
84+
switch cd {
85+
case keyErrorStack:
86+
if larr, err = d.DecodeArrayLen(); err != nil {
87+
return nil, err
88+
}
89+
90+
errorStack = make([]BoxError, larr)
91+
92+
for i := 0; i < larr; i++ {
93+
if l1, err = d.DecodeMapLen(); err != nil {
94+
return nil, err
95+
}
96+
97+
for ; l1 > 0; l1-- {
98+
var cd1 int
99+
if cd1, err = d.DecodeInt(); err != nil {
100+
return nil, err
101+
}
102+
switch cd1 {
103+
case keyErrorType:
104+
if errorStack[i].Type, err = d.DecodeString(); err != nil {
105+
return nil, err
106+
}
107+
case keyErrorFile:
108+
if errorStack[i].File, err = d.DecodeString(); err != nil {
109+
return nil, err
110+
}
111+
case keyErrorLine:
112+
if errorStack[i].Line, err = d.DecodeUint64(); err != nil {
113+
return nil, err
114+
}
115+
case keyErrorMessage:
116+
if errorStack[i].Msg, err = d.DecodeString(); err != nil {
117+
return nil, err
118+
}
119+
case keyErrorErrno:
120+
if errorStack[i].Errno, err = d.DecodeUint64(); err != nil {
121+
return nil, err
122+
}
123+
case keyErrorErrcode:
124+
if errorStack[i].Code, err = d.DecodeUint64(); err != nil {
125+
return nil, err
126+
}
127+
case keyErrorFields:
128+
var mapk string
129+
var mapv interface{}
130+
131+
errorStack[i].Fields = make(map[string]interface{})
132+
133+
if l2, err = d.DecodeMapLen(); err != nil {
134+
return nil, err
135+
}
136+
for ; l2 > 0; l2-- {
137+
if mapk, err = d.DecodeString(); err != nil {
138+
return nil, err
139+
}
140+
if mapv, err = d.DecodeInterface(); err != nil {
141+
return nil, err
142+
}
143+
errorStack[i].Fields[mapk] = mapv
144+
}
145+
default:
146+
if err = d.Skip(); err != nil {
147+
return nil, err
148+
}
149+
}
150+
}
151+
152+
if i > 0 {
153+
errorStack[i-1].Prev = &errorStack[i]
154+
}
155+
}
156+
default:
157+
if err = d.Skip(); err != nil {
158+
return nil, err
159+
}
160+
}
161+
}
162+
163+
if len(errorStack) == 0 {
164+
return nil, fmt.Errorf("msgpack: unexpected empty BoxError stack on decode")
165+
}
166+
167+
return &errorStack[0], nil
168+
}
169+
170+
// UnmarshalMsgpack deserializes a BoxError value from a MessagePack
171+
// representation.
172+
func (e *BoxError) UnmarshalMsgpack(b []byte) error {
173+
if e == nil {
174+
panic("cannot unmarshal to a nil pointer")
175+
}
176+
177+
buf := bytes.NewBuffer(b)
178+
dec := newDecoder(buf)
179+
180+
if val, err := decodeBoxError(dec); err != nil {
181+
return err
182+
} else {
183+
*e = *val
184+
return nil
185+
}
186+
}

box_error_test.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package tarantool_test
2+
3+
import (
4+
"regexp"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
. "github.com/tarantool/go-tarantool"
9+
)
10+
11+
var samples = map[string]BoxError{
12+
"SimpleError": {
13+
Type: "ClientError",
14+
File: "config.lua",
15+
Line: uint64(202),
16+
Msg: "Unknown error",
17+
Errno: uint64(0),
18+
Code: uint64(0),
19+
},
20+
"AccessDeniedError": {
21+
Type: "AccessDeniedError",
22+
File: "/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c",
23+
Line: uint64(535),
24+
Msg: "Execute access to function 'forbidden_function' is denied for user 'no_grants'",
25+
Errno: uint64(0),
26+
Code: uint64(42),
27+
Fields: map[string]interface{}{
28+
"object_type": "function",
29+
"object_name": "forbidden_function",
30+
"access_type": "Execute",
31+
},
32+
},
33+
"ChainedError": {
34+
Type: "ClientError",
35+
File: "config.lua",
36+
Line: uint64(205),
37+
Msg: "Timeout exceeded",
38+
Errno: uint64(0),
39+
Code: uint64(78),
40+
Prev: &BoxError{
41+
Type: "ClientError",
42+
File: "config.lua",
43+
Line: uint64(202),
44+
Msg: "Unknown error",
45+
Errno: uint64(0),
46+
Code: uint64(0),
47+
},
48+
},
49+
}
50+
51+
var stringCases = map[string]struct {
52+
e BoxError
53+
s string
54+
}{
55+
"SimpleError": {
56+
samples["SimpleError"],
57+
"Unknown error (ClientError, code 0x0), see config.lua line 202",
58+
},
59+
"AccessDeniedError": {
60+
samples["AccessDeniedError"],
61+
"Execute access to function 'forbidden_function' is denied for user " +
62+
"'no_grants' (AccessDeniedError, code 0x2a), see " +
63+
"/__w/sdk/sdk/tarantool-2.10/tarantool/src/box/func.c line 535",
64+
},
65+
"ChainedError": {
66+
samples["ChainedError"],
67+
"Timeout exceeded (ClientError, code 0x4e), see config.lua line 205: " +
68+
"Unknown error (ClientError, code 0x0), see config.lua line 202",
69+
},
70+
}
71+
72+
func TestBoxErrorStringRepr(t *testing.T) {
73+
for name, testcase := range stringCases {
74+
t.Run(name, func(t *testing.T) {
75+
require.Equal(t, testcase.s, testcase.e.Error())
76+
})
77+
}
78+
}
79+
80+
var mpDecodeSamples = map[string]struct {
81+
b []byte
82+
ok bool
83+
err *regexp.Regexp
84+
}{
85+
"OuterMapInvalidLen": {
86+
[]byte{0xc1},
87+
false,
88+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`),
89+
},
90+
"OuterMapInvalidKey": {
91+
[]byte{0x81, 0xc1},
92+
false,
93+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding int64`),
94+
},
95+
"OuterMapExtraKey": {
96+
[]byte{0x82, 0x00, 0x91, 0x81, 0x02, 0x01, 0x11, 0x00},
97+
true,
98+
regexp.MustCompile(``),
99+
},
100+
"OuterMapExtraInvalidKey": {
101+
[]byte{0x81, 0x11, 0x81},
102+
false,
103+
regexp.MustCompile(`EOF`),
104+
},
105+
"ArrayInvalidLen": {
106+
[]byte{0x81, 0x00, 0xc1},
107+
false,
108+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding array length`),
109+
},
110+
"ArrayZeroLen": {
111+
[]byte{0x81, 0x00, 0x90},
112+
false,
113+
regexp.MustCompile(`msgpack: unexpected empty BoxError stack on decode`),
114+
},
115+
"InnerMapInvalidLen": {
116+
[]byte{0x81, 0x00, 0x91, 0xc1},
117+
false,
118+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`),
119+
},
120+
"InnerMapInvalidKey": {
121+
[]byte{0x81, 0x00, 0x91, 0x81, 0xc1},
122+
false,
123+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding int64`),
124+
},
125+
"InnerMapInvalidErrorType": {
126+
[]byte{0x81, 0x00, 0x91, 0x81, 0x00, 0xc1},
127+
false,
128+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`),
129+
},
130+
"InnerMapInvalidErrorFile": {
131+
[]byte{0x81, 0x00, 0x91, 0x81, 0x01, 0xc1},
132+
false,
133+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`),
134+
},
135+
"InnerMapInvalidErrorLine": {
136+
[]byte{0x81, 0x00, 0x91, 0x81, 0x02, 0xc1},
137+
false,
138+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`),
139+
},
140+
"InnerMapInvalidErrorMessage": {
141+
[]byte{0x81, 0x00, 0x91, 0x81, 0x03, 0xc1},
142+
false,
143+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`),
144+
},
145+
"InnerMapInvalidErrorErrno": {
146+
[]byte{0x81, 0x00, 0x91, 0x81, 0x04, 0xc1},
147+
false,
148+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`),
149+
},
150+
"InnerMapInvalidErrorErrcode": {
151+
[]byte{0x81, 0x00, 0x91, 0x81, 0x05, 0xc1},
152+
false,
153+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding uint64`),
154+
},
155+
"InnerMapInvalidErrorFields": {
156+
[]byte{0x81, 0x00, 0x91, 0x81, 0x06, 0xc1},
157+
false,
158+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding map length`),
159+
},
160+
"InnerMapInvalidErrorFieldsKey": {
161+
[]byte{0x81, 0x00, 0x91, 0x81, 0x06, 0x81, 0xc1},
162+
false,
163+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding (?:string\/bytes|bytes) length`),
164+
},
165+
"InnerMapInvalidErrorFieldsValue": {
166+
[]byte{0x81, 0x00, 0x91, 0x81, 0x06, 0x81, 0xa3, 0x6b, 0x65, 0x79, 0xc1},
167+
false,
168+
regexp.MustCompile(`msgpack: (?:unexpected|invalid|unknown) code.c1 decoding interface{}`),
169+
},
170+
"InnerMapExtraKey": {
171+
[]byte{0x81, 0x00, 0x91, 0x81, 0x21, 0x00},
172+
true,
173+
regexp.MustCompile(``),
174+
},
175+
"InnerMapExtraInvalidKey": {
176+
[]byte{0x81, 0x00, 0x91, 0x81, 0x21, 0x81},
177+
false,
178+
regexp.MustCompile(`EOF`),
179+
},
180+
}
181+
182+
func TestMessagePackDecode(t *testing.T) {
183+
for name, testcase := range mpDecodeSamples {
184+
t.Run(name, func(t *testing.T) {
185+
var val *BoxError = &BoxError{}
186+
err := val.UnmarshalMsgpack(testcase.b)
187+
if testcase.ok {
188+
require.Nilf(t, err, "No errors on decode")
189+
} else {
190+
require.Regexp(t, testcase.err, err.Error())
191+
}
192+
})
193+
}
194+
}
195+
196+
func TestMessagePackUnmarshalToNil(t *testing.T) {
197+
var val *BoxError = nil
198+
require.PanicsWithValue(t, "cannot unmarshal to a nil pointer",
199+
func() { val.UnmarshalMsgpack(mpDecodeSamples["InnerMapExtraKey"].b) })
200+
}

0 commit comments

Comments
 (0)