Skip to content

Commit 9ae0e98

Browse files
api: support errors extra information
Since Tarantool 2.4.1, iproto error responses contain extra info with backtrace [1]. After this patch, Error would contain ExtraInfo field (BoxError object), if it was provided. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/#responses-for-errors Part of #209
1 parent 7a63b19 commit 9ae0e98

File tree

5 files changed

+250
-5
lines changed

5 files changed

+250
-5
lines changed

const.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ const (
3535
KeyExpression = 0x27
3636
KeyDefTuple = 0x28
3737
KeyData = 0x30
38-
KeyError24 = 0x31
38+
KeyError24 = 0x31 /* Error in pre-2.4 format */
3939
KeyMetaData = 0x32
4040
KeyBindCount = 0x34
4141
KeySQLText = 0x40
4242
KeySQLBind = 0x41
4343
KeySQLInfo = 0x42
4444
KeyStmtID = 0x43
45+
KeyError = 0x52 /* Extended error in >= 2.4 format. */
4546
KeyVersion = 0x54
4647
KeyFeatures = 0x55
4748
KeyTimeout = 0x56
@@ -56,6 +57,15 @@ const (
5657
KeySQLInfoRowCount = 0x00
5758
KeySQLInfoAutoincrementIds = 0x01
5859

60+
KeyErrorStack = 0x00
61+
KeyErrorType = 0x00
62+
KeyErrorFile = 0x01
63+
KeyErrorLine = 0x02
64+
KeyErrorMessage = 0x03
65+
KeyErrorErrno = 0x04
66+
KeyErrorErrcode = 0x05
67+
KeyErrorFields = 0x06
68+
5969
// https://github.com/fl00r/go-tarantool-1.6/issues/2
6070

6171
IterEq = uint32(0) // key == x ASC order

errors.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,25 @@ import "fmt"
44

55
// Error is wrapper around error returned by Tarantool.
66
type Error struct {
7-
Code uint32
8-
Msg string
7+
Code uint32
8+
Msg string
9+
ExtraInfo *BoxError
10+
}
11+
12+
// BoxError is a type representing Tarantool `box.error` object: a single
13+
// MP_ERROR_STACK object with a link to the previous stack error.
14+
type BoxError struct {
15+
Type string // Type that implies source, for example "ClientError".
16+
File string // Source code file where error was caught.
17+
Line int64 // Line number in source code file.
18+
Message string // Text of reason.
19+
Errno int64 // Ordinal number of the error.
20+
Errcode int64 // Number of the error as defined in `errcode.h`.
21+
// Additional fields depending on error type. For example, if
22+
// type is "AccessDeniedError", then it will include "object_type",
23+
// "object_name", "access_type".
24+
Fields map[interface{}]interface{}
25+
Prev *BoxError // Previous error in stack.
926
}
1027

1128
// Error converts an Error to a string.

response.go

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,103 @@ func (resp *Response) smallInt(d *decoder) (i int, err error) {
109109
return d.DecodeInt()
110110
}
111111

112+
func decodeBoxError(d *decoder) (*BoxError, error) {
113+
var l, larr, l1, l2 int
114+
var errorStack []BoxError
115+
var err error
116+
var mapk, mapv interface{}
117+
118+
if l, err = d.DecodeMapLen(); err != nil {
119+
return nil, err
120+
}
121+
122+
for ; l > 0; l-- {
123+
var cd int
124+
if cd, err = d.DecodeInt(); err != nil {
125+
return nil, err
126+
}
127+
switch cd {
128+
case KeyErrorStack:
129+
if larr, err = d.DecodeArrayLen(); err != nil {
130+
return nil, err
131+
}
132+
133+
errorStack = make([]BoxError, larr)
134+
135+
for i := 0; i < larr; i++ {
136+
if l1, err = d.DecodeMapLen(); err != nil {
137+
return nil, err
138+
}
139+
140+
for ; l1 > 0; l1-- {
141+
var cd1 int
142+
if cd1, err = d.DecodeInt(); err != nil {
143+
return nil, err
144+
}
145+
switch cd1 {
146+
case KeyErrorType:
147+
if errorStack[i].Type, err = d.DecodeString(); err != nil {
148+
return nil, err
149+
}
150+
case KeyErrorFile:
151+
if errorStack[i].File, err = d.DecodeString(); err != nil {
152+
return nil, err
153+
}
154+
case KeyErrorLine:
155+
if errorStack[i].Line, err = d.DecodeInt64(); err != nil {
156+
return nil, err
157+
}
158+
case KeyErrorMessage:
159+
if errorStack[i].Message, err = d.DecodeString(); err != nil {
160+
return nil, err
161+
}
162+
case KeyErrorErrno:
163+
if errorStack[i].Errno, err = d.DecodeInt64(); err != nil {
164+
return nil, err
165+
}
166+
case KeyErrorErrcode:
167+
if errorStack[i].Errcode, err = d.DecodeInt64(); err != nil {
168+
return nil, err
169+
}
170+
case KeyErrorFields:
171+
errorStack[i].Fields = make(map[interface{}]interface{})
172+
if l2, err = d.DecodeMapLen(); err != nil {
173+
return nil, err
174+
}
175+
for ; l2 > 0; l2-- {
176+
if mapk, err = d.DecodeInterface(); err != nil {
177+
return nil, err
178+
}
179+
if mapv, err = d.DecodeInterface(); err != nil {
180+
return nil, err
181+
}
182+
errorStack[i].Fields[mapk] = mapv
183+
}
184+
default:
185+
if err = d.Skip(); err != nil {
186+
return nil, err
187+
}
188+
}
189+
}
190+
191+
if i > 0 {
192+
errorStack[i-1].Prev = &errorStack[i]
193+
}
194+
}
195+
default:
196+
if err = d.Skip(); err != nil {
197+
return nil, err
198+
}
199+
}
200+
}
201+
202+
if len(errorStack) > 0 {
203+
return &errorStack[0], nil
204+
}
205+
206+
return nil, nil
207+
}
208+
112209
func (resp *Response) decodeHeader(d *decoder) (err error) {
113210
var l int
114211
d.Reset(&resp.buf)
@@ -154,6 +251,7 @@ func (resp *Response) decodeBody() (err error) {
154251
features: []Feature{},
155252
}
156253
var feature Feature
254+
var extraErrorInfo *BoxError = nil
157255
isIdResponse := false
158256

159257
d := newDecoder(&resp.buf)
@@ -180,6 +278,10 @@ func (resp *Response) decodeBody() (err error) {
180278
if resp.Error, err = d.DecodeString(); err != nil {
181279
return err
182280
}
281+
case KeyError:
282+
if extraErrorInfo, err = decodeBoxError(d); err != nil {
283+
return err
284+
}
183285
case KeySQLInfo:
184286
if err = d.Decode(&resp.SQLInfo); err != nil {
185287
return err
@@ -235,7 +337,7 @@ func (resp *Response) decodeBody() (err error) {
235337

236338
if resp.Code != OkCode && resp.Code != PushCode {
237339
resp.Code &^= ErrorCodeBit
238-
err = Error{resp.Code, resp.Error}
340+
err = Error{resp.Code, resp.Error, extraErrorInfo}
239341
}
240342
}
241343
return
@@ -247,6 +349,8 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) {
247349
defer resp.buf.Seek(offset)
248350

249351
var l int
352+
var extraErrorInfo *BoxError = nil
353+
250354
d := newDecoder(&resp.buf)
251355
if l, err = d.DecodeMapLen(); err != nil {
252356
return err
@@ -265,6 +369,10 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) {
265369
if resp.Error, err = d.DecodeString(); err != nil {
266370
return err
267371
}
372+
case KeyError:
373+
if extraErrorInfo, err = decodeBoxError(d); err != nil {
374+
return err
375+
}
268376
case KeySQLInfo:
269377
if err = d.Decode(&resp.SQLInfo); err != nil {
270378
return err
@@ -281,7 +389,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) {
281389
}
282390
if resp.Code != OkCode && resp.Code != PushCode {
283391
resp.Code &^= ErrorCodeBit
284-
err = Error{resp.Code, resp.Error}
392+
err = Error{resp.Code, resp.Error, extraErrorInfo}
285393
}
286394
}
287395
return

tarantool_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2940,6 +2940,102 @@ func TestConnectionFeatureRequirementServer(t *testing.T) {
29402940
}
29412941
}
29422942

2943+
func TestExtraErrorInfoBasic(t *testing.T) {
2944+
test_helpers.SkipIfErrorExtraInfoUnsupported(t)
2945+
2946+
conn := test_helpers.ConnectWithValidation(t, server, opts)
2947+
defer conn.Close()
2948+
2949+
_, err := conn.Eval("not a Lua code", []interface{}{})
2950+
require.NotNilf(t, err, "expected error on invalid Lua code")
2951+
2952+
terr, ok := err.(Error)
2953+
require.Equalf(t, ok, true, "error is built from a Tarantool error")
2954+
2955+
require.NotNilf(t, terr.ExtraInfo, "error provides extra info")
2956+
require.Equal(t, terr.ExtraInfo.Type, "LuajitError")
2957+
// File+Line info may change between any Tarantool releases
2958+
require.Greaterf(t, len(terr.ExtraInfo.File), 0, "file info not empty")
2959+
require.Greaterf(t, terr.ExtraInfo.Line, int64(0), "line info not empty")
2960+
require.Equal(t, terr.ExtraInfo.Message, "eval:1: unexpected symbol near 'not'")
2961+
require.Equal(t, terr.ExtraInfo.Errno, int64(0))
2962+
require.Equal(t, terr.ExtraInfo.Errcode, int64(32))
2963+
require.Equal(t, terr.ExtraInfo.Fields, map[interface{}]interface{}(nil))
2964+
require.Nilf(t, terr.ExtraInfo.Prev, "stack contains exactly one error")
2965+
}
2966+
2967+
func TestExtraErrorInfoStacked(t *testing.T) {
2968+
test_helpers.SkipIfErrorExtraInfoUnsupported(t)
2969+
2970+
conn := test_helpers.ConnectWithValidation(t, server, opts)
2971+
defer conn.Close()
2972+
2973+
_, err := conn.Eval(`
2974+
local e1 = box.error.new(box.error.UNKNOWN)
2975+
local e2 = box.error.new(box.error.TIMEOUT)
2976+
e2:set_prev(e1)
2977+
error(e2)`,
2978+
[]interface{}{})
2979+
require.NotNilf(t, err, "expected error on explicit error raise")
2980+
2981+
terr, ok := err.(Error)
2982+
require.Equalf(t, ok, true, "error is built from a Tarantool error")
2983+
2984+
require.NotNilf(t, terr.ExtraInfo, "error provides extra info")
2985+
require.Equal(t, terr.ExtraInfo.Type, "ClientError")
2986+
require.Greaterf(t, len(terr.ExtraInfo.File), 0, "file info not empty")
2987+
require.Equal(t, terr.ExtraInfo.Line, int64(3))
2988+
require.Equal(t, terr.ExtraInfo.Message, "Timeout exceeded")
2989+
require.Equal(t, terr.ExtraInfo.Errno, int64(0))
2990+
require.Equal(t, terr.ExtraInfo.Errcode, int64(78))
2991+
require.Equal(t, terr.ExtraInfo.Fields, map[interface{}]interface{}(nil))
2992+
2993+
prevExtraInfo := terr.ExtraInfo.Prev
2994+
require.NotNilf(t, prevExtraInfo, "stack contains more than one error")
2995+
require.Equal(t, prevExtraInfo.Type, "ClientError")
2996+
require.Greaterf(t, len(prevExtraInfo.File), 0, "file info not empty")
2997+
require.Equal(t, prevExtraInfo.Line, int64(2))
2998+
require.Equal(t, prevExtraInfo.Message, "Unknown error")
2999+
require.Equal(t, prevExtraInfo.Errno, int64(0))
3000+
require.Equal(t, prevExtraInfo.Errcode, int64(0))
3001+
require.Equal(t, prevExtraInfo.Fields, map[interface{}]interface{}(nil))
3002+
3003+
require.Nilf(t, prevExtraInfo.Prev, "stack contains exactly two errors")
3004+
}
3005+
3006+
func TestExtraErrorInfoFields(t *testing.T) {
3007+
test_helpers.SkipIfErrorExtraInfoUnsupported(t)
3008+
3009+
conn := test_helpers.ConnectWithValidation(t, server, opts)
3010+
defer conn.Close()
3011+
3012+
// "test" user cannot create functions
3013+
_, err := conn.Eval("box.schema.func.create('forbidden_function')", []interface{}{})
3014+
require.NotNilf(t, err, "expected error on forbidden action")
3015+
3016+
terr, ok := err.(Error)
3017+
require.Equalf(t, ok, true, "error is built from a Tarantool error")
3018+
3019+
require.NotNilf(t, terr.ExtraInfo, "error provides extra info")
3020+
require.Equal(t, terr.ExtraInfo.Type, "AccessDeniedError")
3021+
// File+Line info may change between any Tarantool releases
3022+
require.Greaterf(t, len(terr.ExtraInfo.File), 0, "file info not empty")
3023+
require.Greaterf(t, terr.ExtraInfo.Line, int64(0), "line info not empty")
3024+
require.Equal(t,
3025+
terr.ExtraInfo.Message,
3026+
"Create access to function 'forbidden_function' is denied for user 'test'")
3027+
require.Equal(t, terr.ExtraInfo.Errno, int64(0))
3028+
require.Equal(t, terr.ExtraInfo.Errcode, int64(42))
3029+
require.Equal(t,
3030+
terr.ExtraInfo.Fields,
3031+
map[interface{}]interface{}{
3032+
"object_type": "function",
3033+
"object_name": "forbidden_function",
3034+
"access_type": "Create",
3035+
})
3036+
require.Nilf(t, terr.ExtraInfo.Prev, "stack contains exactly one error")
3037+
}
3038+
29433039
// runTestMain is a body of TestMain function
29443040
// (see https://pkg.go.dev/testing#hdr-Main).
29453041
// Using defer + os.Exit is not works so TestMain body

test_helpers/utils.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,17 @@ func SkipIfIdSupported(t *testing.T) {
8080
t.Skip("Skipping test for Tarantool with non-zero protocol version and features")
8181
}
8282
}
83+
84+
func SkipIfErrorExtraInfoUnsupported(t *testing.T) {
85+
t.Helper()
86+
87+
// Tarantool provides extra error info only since 2.4.1 version
88+
isLess, err := IsTarantoolVersionLess(2, 4, 1)
89+
if err != nil {
90+
t.Fatalf("Could not check the Tarantool version")
91+
}
92+
93+
if isLess {
94+
t.Skip("Skipping test for Tarantool without extra error info")
95+
}
96+
}

0 commit comments

Comments
 (0)