Skip to content

Commit 9538a37

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 500da07 commit 9538a37

9 files changed

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

box_error_test.go

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

0 commit comments

Comments
 (0)