Skip to content

Commit 6ff1c75

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 3afc90d commit 6ff1c75

9 files changed

+584
-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: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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{0x81, 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+
true,
113+
regexp.MustCompile(``),
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
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+
}

0 commit comments

Comments
 (0)