diff --git a/const.go b/const.go index 8cd9bc490..12be51d64 100644 --- a/const.go +++ b/const.go @@ -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 @@ -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 diff --git a/errors.go b/errors.go index 5677d07fc..33a46d062 100644 --- a/errors.go +++ b/errors.go @@ -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. diff --git a/response.go b/response.go index 54b4e7be7..1ca2c1797 100644 --- a/response.go +++ b/response.go @@ -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) @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/tarantool_test.go b/tarantool_test.go index a99fb4021..55624a0cc 100644 --- a/tarantool_test.go +++ b/tarantool_test.go @@ -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 diff --git a/test_helpers/utils.go b/test_helpers/utils.go index aabfd73bc..da9349d63 100644 --- a/test_helpers/utils.go +++ b/test_helpers/utils.go @@ -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") + } +}