Skip to content

Commit

Permalink
api: support errors extra information
Browse files Browse the repository at this point in the history
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
  • Loading branch information
DifferentialOrange committed Nov 16, 2022
1 parent 7a63b19 commit 9ae0e98
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 5 deletions.
12 changes: 11 additions & 1 deletion const.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ const (
KeyExpression = 0x27
KeyDefTuple = 0x28
KeyData = 0x30
KeyError24 = 0x31
KeyError24 = 0x31 /* Error in pre-2.4 format */
KeyMetaData = 0x32
KeyBindCount = 0x34
KeySQLText = 0x40
KeySQLBind = 0x41
KeySQLInfo = 0x42
KeyStmtID = 0x43
KeyError = 0x52 /* Extended error in >= 2.4 format. */
KeyVersion = 0x54
KeyFeatures = 0x55
KeyTimeout = 0x56
Expand All @@ -56,6 +57,15 @@ const (
KeySQLInfoRowCount = 0x00
KeySQLInfoAutoincrementIds = 0x01

KeyErrorStack = 0x00
KeyErrorType = 0x00
KeyErrorFile = 0x01
KeyErrorLine = 0x02
KeyErrorMessage = 0x03
KeyErrorErrno = 0x04
KeyErrorErrcode = 0x05
KeyErrorFields = 0x06

// https://github.com/fl00r/go-tarantool-1.6/issues/2

IterEq = uint32(0) // key == x ASC order
Expand Down
21 changes: 19 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,25 @@ import "fmt"

// Error is wrapper around error returned by Tarantool.
type Error struct {
Code uint32
Msg string
Code uint32
Msg string
ExtraInfo *BoxError
}

// BoxError is a type representing Tarantool `box.error` object: a single
// MP_ERROR_STACK object with a link to the previous stack error.
type BoxError struct {
Type string // Type that implies source, for example "ClientError".
File string // Source code file where error was caught.
Line int64 // Line number in source code file.
Message string // Text of reason.
Errno int64 // Ordinal number of the error.
Errcode int64 // Number of the error as defined in `errcode.h`.
// Additional fields depending on error type. For example, if
// type is "AccessDeniedError", then it will include "object_type",
// "object_name", "access_type".
Fields map[interface{}]interface{}
Prev *BoxError // Previous error in stack.
}

// Error converts an Error to a string.
Expand Down
112 changes: 110 additions & 2 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,103 @@ func (resp *Response) smallInt(d *decoder) (i int, err error) {
return d.DecodeInt()
}

func decodeBoxError(d *decoder) (*BoxError, error) {
var l, larr, l1, l2 int
var errorStack []BoxError
var err error
var mapk, mapv interface{}

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.DecodeInt64(); err != nil {
return nil, err
}
case KeyErrorMessage:
if errorStack[i].Message, err = d.DecodeString(); err != nil {
return nil, err
}
case KeyErrorErrno:
if errorStack[i].Errno, err = d.DecodeInt64(); err != nil {
return nil, err
}
case KeyErrorErrcode:
if errorStack[i].Errcode, err = d.DecodeInt64(); err != nil {
return nil, err
}
case KeyErrorFields:
errorStack[i].Fields = make(map[interface{}]interface{})
if l2, err = d.DecodeMapLen(); err != nil {
return nil, err
}
for ; l2 > 0; l2-- {
if mapk, err = d.DecodeInterface(); 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 &errorStack[0], nil
}

return nil, nil
}

func (resp *Response) decodeHeader(d *decoder) (err error) {
var l int
d.Reset(&resp.buf)
Expand Down Expand Up @@ -154,6 +251,7 @@ func (resp *Response) decodeBody() (err error) {
features: []Feature{},
}
var feature Feature
var extraErrorInfo *BoxError = nil
isIdResponse := false

d := newDecoder(&resp.buf)
Expand All @@ -180,6 +278,10 @@ func (resp *Response) decodeBody() (err error) {
if resp.Error, err = d.DecodeString(); err != nil {
return err
}
case KeyError:
if extraErrorInfo, err = decodeBoxError(d); err != nil {
return err
}
case KeySQLInfo:
if err = d.Decode(&resp.SQLInfo); err != nil {
return err
Expand Down Expand Up @@ -235,7 +337,7 @@ func (resp *Response) decodeBody() (err error) {

if resp.Code != OkCode && resp.Code != PushCode {
resp.Code &^= ErrorCodeBit
err = Error{resp.Code, resp.Error}
err = Error{resp.Code, resp.Error, extraErrorInfo}
}
}
return
Expand All @@ -247,6 +349,8 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) {
defer resp.buf.Seek(offset)

var l int
var extraErrorInfo *BoxError = nil

d := newDecoder(&resp.buf)
if l, err = d.DecodeMapLen(); err != nil {
return err
Expand All @@ -265,6 +369,10 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) {
if resp.Error, err = d.DecodeString(); err != nil {
return err
}
case KeyError:
if extraErrorInfo, err = decodeBoxError(d); err != nil {
return err
}
case KeySQLInfo:
if err = d.Decode(&resp.SQLInfo); err != nil {
return err
Expand All @@ -281,7 +389,7 @@ func (resp *Response) decodeBodyTyped(res interface{}) (err error) {
}
if resp.Code != OkCode && resp.Code != PushCode {
resp.Code &^= ErrorCodeBit
err = Error{resp.Code, resp.Error}
err = Error{resp.Code, resp.Error, extraErrorInfo}
}
}
return
Expand Down
96 changes: 96 additions & 0 deletions tarantool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2940,6 +2940,102 @@ func TestConnectionFeatureRequirementServer(t *testing.T) {
}
}

func TestExtraErrorInfoBasic(t *testing.T) {
test_helpers.SkipIfErrorExtraInfoUnsupported(t)

conn := test_helpers.ConnectWithValidation(t, server, opts)
defer conn.Close()

_, err := conn.Eval("not a Lua code", []interface{}{})
require.NotNilf(t, err, "expected error on invalid Lua code")

terr, ok := err.(Error)
require.Equalf(t, ok, true, "error is built from a Tarantool error")

require.NotNilf(t, terr.ExtraInfo, "error provides extra info")
require.Equal(t, terr.ExtraInfo.Type, "LuajitError")
// File+Line info may change between any Tarantool releases
require.Greaterf(t, len(terr.ExtraInfo.File), 0, "file info not empty")
require.Greaterf(t, terr.ExtraInfo.Line, int64(0), "line info not empty")
require.Equal(t, terr.ExtraInfo.Message, "eval:1: unexpected symbol near 'not'")
require.Equal(t, terr.ExtraInfo.Errno, int64(0))
require.Equal(t, terr.ExtraInfo.Errcode, int64(32))
require.Equal(t, terr.ExtraInfo.Fields, map[interface{}]interface{}(nil))
require.Nilf(t, terr.ExtraInfo.Prev, "stack contains exactly one error")
}

func TestExtraErrorInfoStacked(t *testing.T) {
test_helpers.SkipIfErrorExtraInfoUnsupported(t)

conn := test_helpers.ConnectWithValidation(t, server, opts)
defer conn.Close()

_, err := conn.Eval(`
local e1 = box.error.new(box.error.UNKNOWN)
local e2 = box.error.new(box.error.TIMEOUT)
e2:set_prev(e1)
error(e2)`,
[]interface{}{})
require.NotNilf(t, err, "expected error on explicit error raise")

terr, ok := err.(Error)
require.Equalf(t, ok, true, "error is built from a Tarantool error")

require.NotNilf(t, terr.ExtraInfo, "error provides extra info")
require.Equal(t, terr.ExtraInfo.Type, "ClientError")
require.Greaterf(t, len(terr.ExtraInfo.File), 0, "file info not empty")
require.Equal(t, terr.ExtraInfo.Line, int64(3))
require.Equal(t, terr.ExtraInfo.Message, "Timeout exceeded")
require.Equal(t, terr.ExtraInfo.Errno, int64(0))
require.Equal(t, terr.ExtraInfo.Errcode, int64(78))
require.Equal(t, terr.ExtraInfo.Fields, map[interface{}]interface{}(nil))

prevExtraInfo := terr.ExtraInfo.Prev
require.NotNilf(t, prevExtraInfo, "stack contains more than one error")
require.Equal(t, prevExtraInfo.Type, "ClientError")
require.Greaterf(t, len(prevExtraInfo.File), 0, "file info not empty")
require.Equal(t, prevExtraInfo.Line, int64(2))
require.Equal(t, prevExtraInfo.Message, "Unknown error")
require.Equal(t, prevExtraInfo.Errno, int64(0))
require.Equal(t, prevExtraInfo.Errcode, int64(0))
require.Equal(t, prevExtraInfo.Fields, map[interface{}]interface{}(nil))

require.Nilf(t, prevExtraInfo.Prev, "stack contains exactly two errors")
}

func TestExtraErrorInfoFields(t *testing.T) {
test_helpers.SkipIfErrorExtraInfoUnsupported(t)

conn := test_helpers.ConnectWithValidation(t, server, opts)
defer conn.Close()

// "test" user cannot create functions
_, err := conn.Eval("box.schema.func.create('forbidden_function')", []interface{}{})
require.NotNilf(t, err, "expected error on forbidden action")

terr, ok := err.(Error)
require.Equalf(t, ok, true, "error is built from a Tarantool error")

require.NotNilf(t, terr.ExtraInfo, "error provides extra info")
require.Equal(t, terr.ExtraInfo.Type, "AccessDeniedError")
// File+Line info may change between any Tarantool releases
require.Greaterf(t, len(terr.ExtraInfo.File), 0, "file info not empty")
require.Greaterf(t, terr.ExtraInfo.Line, int64(0), "line info not empty")
require.Equal(t,
terr.ExtraInfo.Message,
"Create access to function 'forbidden_function' is denied for user 'test'")
require.Equal(t, terr.ExtraInfo.Errno, int64(0))
require.Equal(t, terr.ExtraInfo.Errcode, int64(42))
require.Equal(t,
terr.ExtraInfo.Fields,
map[interface{}]interface{}{
"object_type": "function",
"object_name": "forbidden_function",
"access_type": "Create",
})
require.Nilf(t, terr.ExtraInfo.Prev, "stack contains exactly one error")
}

// runTestMain is a body of TestMain function
// (see https://pkg.go.dev/testing#hdr-Main).
// Using defer + os.Exit is not works so TestMain body
Expand Down
14 changes: 14 additions & 0 deletions test_helpers/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,17 @@ func SkipIfIdSupported(t *testing.T) {
t.Skip("Skipping test for Tarantool with non-zero protocol version and features")
}
}

func SkipIfErrorExtraInfoUnsupported(t *testing.T) {
t.Helper()

// Tarantool provides extra error info only since 2.4.1 version
isLess, err := IsTarantoolVersionLess(2, 4, 1)
if err != nil {
t.Fatalf("Could not check the Tarantool version")
}

if isLess {
t.Skip("Skipping test for Tarantool without extra error info")
}
}

0 comments on commit 9ae0e98

Please sign in to comment.