diff --git a/changelogs/unreleased/gh-7968-mp-check-diag.md b/changelogs/unreleased/gh-7968-mp-check-diag.md new file mode 100644 index 0000000000000000000000000000000000000000..1e2345b8f47dc4a0f6b49073e0e5c7792936504e --- /dev/null +++ b/changelogs/unreleased/gh-7968-mp-check-diag.md @@ -0,0 +1,7 @@ +## feature/lua/msgpack + +* Improved error reporting for `msgpack.decode`. Now, an error raised by + `mgpack.decode` has a detailed error message and the offset in the input + data. If `msgpack.decode` failed to unpack a MsgPack extension, it also + includes the error cause pointing to the error in the extension data + (gh-7986). diff --git a/src/box/mp_error.cc b/src/box/mp_error.cc index a6219433e92ab78aff71efcf4b91caf352b305f8..511c1ecd1daf1ffc9bc7dfdf6bf979865416c66a 100644 --- a/src/box/mp_error.cc +++ b/src/box/mp_error.cc @@ -42,6 +42,7 @@ #include "mp_extension_types.h" #include "fiber.h" #include "ssl_error.h" +#include "trivia/util.h" /** * MP_ERROR format: @@ -207,13 +208,21 @@ static struct error * error_build(struct mp_error *mp_error) { struct error *err = NULL; - if (mp_error->type == NULL || mp_error->message == NULL || - mp_error->file == NULL) { + if (mp_error->type == NULL) { diag_set(ClientError, ER_INVALID_MSGPACK, - "Missing mandatory error fields"); + "MP_ERROR_TYPE is missing"); + return NULL; + } + if (mp_error->message == NULL) { + diag_set(ClientError, ER_INVALID_MSGPACK, + "MP_ERROR_MESSAGE is missing"); + return NULL; + } + if (mp_error->file == NULL) { + diag_set(ClientError, ER_INVALID_MSGPACK, + "MP_ERROR_FILE is missing"); return NULL; } - if (strcmp(mp_error->type, "ClientError") == 0) { err = new ClientError(); } else if (strcmp(mp_error->type, "CustomError") == 0) { @@ -261,44 +270,43 @@ error_build(struct mp_error *mp_error) return err; } -static inline const char * -region_strdup(struct region *region, const char *str, uint32_t len) -{ - char *res = (char *)region_alloc(region, len + 1); - if (res == NULL) { - diag_set(OutOfMemory, len + 1, "region_alloc", "res"); - return NULL; - } - memcpy(res, str, len); - res[len] = 0; - return res; -} - static inline const char * mp_decode_and_copy_str(const char **data, struct region *region) { - if (mp_typeof(**data) != MP_STR) { - diag_set(ClientError, ER_INVALID_MSGPACK, - "Invalid MP_ERROR MsgPack format"); - return NULL; - } uint32_t str_len; const char *str = mp_decode_str(data, &str_len); - return region_strdup(region, str, str_len);; + char *copy = (char *)xregion_alloc(region, str_len + 1); + memcpy(copy, str, str_len); + copy[str_len] = '\0'; + return copy; } +/** + * Helper macro for checking that MsgPack data has a particular type. + * Sets diag and returns -1 on error. + */ +#define CHECK_MP_TYPE(data, type, what) ({ \ + int rc = 0; \ + if (mp_typeof(**(data)) != (type)) { \ + diag_set(ClientError, ER_INVALID_MSGPACK, \ + what " must be " #type); \ + rc = -1; \ + } \ + rc; \ +}) + static int mp_decode_error_fields(const char **data, struct mp_error *mp_err, struct region *region) { - if (mp_typeof(**data) != MP_MAP) + if (CHECK_MP_TYPE(data, MP_MAP, "MP_ERROR_FIELDS value") != 0) return -1; uint32_t map_sz = mp_decode_map(data); for (uint32_t i = 0; i < map_sz; ++i) { uint32_t svp = region_used(region); - const char *key = mp_decode_and_copy_str(data, region); - if (key == NULL) + if (CHECK_MP_TYPE(data, MP_STR, "error payload field name")) return -1; + const char *key = mp_decode_and_copy_str(data, region); const char *value = *data; mp_next(data); uint32_t value_len = *data - value; @@ -318,44 +326,48 @@ mp_decode_error_one(const char **data) struct error *err = NULL; uint32_t map_size; - if (mp_typeof(**data) != MP_MAP) - goto error; - + if (CHECK_MP_TYPE(data, MP_MAP, "MP_ERROR_STACK array entry") != 0) + goto finish; map_size = mp_decode_map(data); for (uint32_t i = 0; i < map_size; ++i) { - if (mp_typeof(**data) != MP_UINT) - goto error; - + if (CHECK_MP_TYPE(data, MP_UINT, "error field key") != 0) + goto finish; uint64_t key = mp_decode_uint(data); switch(key) { case MP_ERROR_TYPE: - mp_err.type = mp_decode_and_copy_str(data, region); - if (mp_err.type == NULL) + if (CHECK_MP_TYPE(data, MP_STR, + "MP_ERROR_TYPE value") != 0) goto finish; + mp_err.type = mp_decode_and_copy_str(data, region); break; case MP_ERROR_FILE: - mp_err.file = mp_decode_and_copy_str(data, region); - if (mp_err.file == NULL) + if (CHECK_MP_TYPE(data, MP_STR, + "MP_ERROR_FILE value") != 0) goto finish; + mp_err.file = mp_decode_and_copy_str(data, region); break; case MP_ERROR_LINE: - if (mp_typeof(**data) != MP_UINT) - goto error; + if (CHECK_MP_TYPE(data, MP_UINT, + "MP_ERROR_LINE value") != 0) + goto finish; mp_err.line = mp_decode_uint(data); break; case MP_ERROR_MESSAGE: - mp_err.message = mp_decode_and_copy_str(data, region); - if (mp_err.message == NULL) + if (CHECK_MP_TYPE(data, MP_STR, + "MP_ERROR_MESSAGE value") != 0) goto finish; + mp_err.message = mp_decode_and_copy_str(data, region); break; case MP_ERROR_ERRNO: - if (mp_typeof(**data) != MP_UINT) - goto error; + if (CHECK_MP_TYPE(data, MP_UINT, + "MP_ERROR_ERRNO value") != 0) + goto finish; mp_err.saved_errno = mp_decode_uint(data); break; case MP_ERROR_CODE: - if (mp_typeof(**data) != MP_UINT) - goto error; + if (CHECK_MP_TYPE(data, MP_UINT, + "MP_ERROR_CODE value") != 0) + goto finish; mp_err.code = mp_decode_uint(data); break; case MP_ERROR_FIELDS: @@ -372,11 +384,6 @@ mp_decode_error_one(const char **data) region_truncate(region, region_svp); mp_error_destroy(&mp_err); return err; - -error: - diag_set(ClientError, ER_INVALID_MSGPACK, - "Invalid MP_ERROR MsgPack format"); - goto finish; } /** @@ -460,28 +467,22 @@ error_to_mpstream(const struct error *error, struct mpstream *stream) struct error * error_unpack_unsafe(const char **data) { + const char *begin = *data; struct error *err = NULL; + uint32_t map_size; - if (mp_typeof(**data) != MP_MAP) { - diag_set(ClientError, ER_INVALID_MSGPACK, - "Invalid MP_ERROR MsgPack format"); - return NULL; - } - uint32_t map_size = mp_decode_map(data); + if (CHECK_MP_TYPE(data, MP_MAP, "error data") != 0) + goto error; + map_size = mp_decode_map(data); for (uint32_t i = 0; i < map_size; ++i) { - if (mp_typeof(**data) != MP_UINT) { - diag_set(ClientError, ER_INVALID_MSGPACK, - "Invalid MP_ERROR MsgPack format"); + if (CHECK_MP_TYPE(data, MP_UINT, "error data key") != 0) goto error; - } uint64_t key = mp_decode_uint(data); switch(key) { case MP_ERROR_STACK: { - if (mp_typeof(**data) != MP_ARRAY) { - diag_set(ClientError, ER_INVALID_MSGPACK, - "Invalid MP_ERROR MsgPack format"); + if (CHECK_MP_TYPE(data, MP_ARRAY, + "MP_ERROR_STACK value") != 0) goto error; - } uint32_t stack_sz = mp_decode_array(data); struct error *effect = NULL; for (uint32_t i = 0; i < stack_sz; i++) { @@ -493,7 +494,6 @@ error_unpack_unsafe(const char **data) effect = cur; continue; } - error_set_prev(effect, cur); effect = cur; } @@ -503,6 +503,11 @@ error_unpack_unsafe(const char **data) mp_next(data); } } + if (err == NULL) { + diag_set(ClientError, ER_INVALID_MSGPACK, + "MP_ERROR_STACK is missing"); + goto error; + } return err; error: @@ -511,6 +516,9 @@ error_unpack_unsafe(const char **data) error_ref(err); error_unref(err); } + err = diag_last_error(diag_get()); + assert(err != NULL); + error_set_uint(err, "offset", *data - begin); return NULL; } @@ -519,7 +527,7 @@ error_unpack(const char **data, uint32_t len) { const char *end = *data + len; const char *check = *data; - if (mp_check(&check, end) != 0 || check != end) + if (mp_check_exact(&check, end) != 0) return NULL; return error_unpack_unsafe(data); } diff --git a/src/box/msgpack.c b/src/box/msgpack.c index d15a6ab45a534bbf7ea50f556e11a1aace2ff36e..59ba761948dccd988f948c932459d5ccaef6386d 100644 --- a/src/box/msgpack.c +++ b/src/box/msgpack.c @@ -39,6 +39,11 @@ #include "mp_interval.h" #include "mp_compression.h" +#include "diag.h" +#include "error.h" +#include "errcode.h" +#include "trivia/util.h" + static int msgpack_fprint_ext(FILE *file, const char **data, int depth) { @@ -95,25 +100,84 @@ msgpack_check_ext_data(int8_t type, const char *data, uint32_t len) { switch (type) { case MP_DECIMAL: - return mp_validate_decimal(data, len); + if (mp_validate_decimal(data, len) != 0) { + diag_set(ClientError, ER_INVALID_MSGPACK, + "cannot unpack decimal"); + return -1; + } + return 0; case MP_UUID: - return mp_validate_uuid(data, len); + if (mp_validate_uuid(data, len) != 0) { + diag_set(ClientError, ER_INVALID_MSGPACK, + "cannot unpack uuid"); + return -1; + } + return 0; case MP_DATETIME: - return mp_validate_datetime(data, len); + if (mp_validate_datetime(data, len) != 0) { + diag_set(ClientError, ER_INVALID_MSGPACK, + "cannot unpack datetime"); + return -1; + } + return 0; case MP_ERROR: - return mp_validate_error(data, len); + if (mp_validate_error(data, len) != 0) { + /* The error is set by error_unpack(). */ + diag_add(ClientError, ER_INVALID_MSGPACK, + "cannot unpack error"); + return -1; + } + return 0; case MP_INTERVAL: - return mp_validate_interval(data, len); + if (mp_validate_interval(data, len) != 0) { + diag_set(ClientError, ER_INVALID_MSGPACK, + "cannot unpack interval"); + return -1; + } + return 0; case MP_COMPRESSION: default: return mp_check_ext_data_default(type, data, len); } } +/** Our handler invoked on mp_check() error. */ +static void +msgpack_check_on_error(const struct mp_check_error *mperr) +{ + struct error *err; + switch (mperr->type) { + case MP_CHECK_ERROR_TRUNC: + err = diag_set(ClientError, ER_INVALID_MSGPACK, + "truncated input"); + error_set_int(err, "trunc_count", mperr->trunc_count); + break; + case MP_CHECK_ERROR_ILL: + err = diag_set(ClientError, ER_INVALID_MSGPACK, + "illegal code"); + break; + case MP_CHECK_ERROR_EXT: + /* The error is set by msgpack_check_ext_data(). */ + err = diag_add(ClientError, ER_INVALID_MSGPACK, + "invalid extension"); + error_set_int(err, "ext_type", mperr->ext_type); + error_set_uint(err, "ext_len", mperr->ext_len); + break; + case MP_CHECK_ERROR_JUNK: + err = diag_set(ClientError, ER_INVALID_MSGPACK, + "junk after input"); + break; + default: + unreachable(); + } + error_set_uint(err, "offset", mperr->pos - mperr->data); +} + void msgpack_init(void) { mp_fprint_ext = msgpack_fprint_ext; mp_snprint_ext = msgpack_snprint_ext; mp_check_ext_data = msgpack_check_ext_data; + mp_check_on_error = msgpack_check_on_error; } diff --git a/src/lua/msgpack.c b/src/lua/msgpack.c index 5868c6a3a9d9b473e9094db614e7e2fbf32ca337..1191a3b85bf2aef09ddcbd62dc3a0e832b6f713f 100644 --- a/src/lua/msgpack.c +++ b/src/lua/msgpack.c @@ -581,7 +581,7 @@ lua_msgpack_decode_cdata(lua_State *L, bool check) } const char *p = data; if (mp_check(&p, data + data_len) != 0) - return luaL_error(L, "msgpack.decode: invalid MsgPack"); + return luaT_error(L); } struct luaL_serializer *cfg = luaL_checkserializer(L); luamp_decode(L, cfg, &data); @@ -604,7 +604,7 @@ lua_msgpack_decode_string(lua_State *L, bool check) if (check) { const char *p = data + offset; if (mp_check(&p, data + data_len) != 0) - return luaL_error(L, "msgpack.decode: invalid MsgPack"); + return luaT_error(L); } struct luaL_serializer *cfg = luaL_checkserializer(L); const char *p = data + offset; @@ -772,7 +772,7 @@ luamp_push_with_translation(struct lua_State *L, const char *data, size_t data_len = data_end - data; struct luamp_object *obj = luamp_new_object(L, data_len); memcpy((char *)obj->data, data, data_len); - assert(mp_check(&data, data_end) == 0 && data == data_end); + assert(mp_check_exact(&data, data_end) == 0); obj->translation = translation; } @@ -828,10 +828,8 @@ lua_msgpack_object_from_raw(struct lua_State *L) } const char *p = data; const char *data_end = data + data_len; - if (mp_check(&p, data_end) != 0 || p != data_end) { - return luaL_error(L, "msgpack.object_from_raw: " - "invalid MsgPack"); - } + if (mp_check_exact(&p, data_end) != 0) + return luaT_error(L); struct luamp_object *obj = luamp_new_object(L, data_len); memcpy((char *)obj->data, data, data_len); obj->cfg = luaL_checkserializer(L); diff --git a/test/app-luatest/msgpack_test.lua b/test/app-luatest/msgpack_test.lua index 73d9e79e84fe7b650ba0a8ab6be0862ccf38dd7c..3ac6e44d2368ac1ee9e3c0ef7f273257032fe044 100644 --- a/test/app-luatest/msgpack_test.lua +++ b/test/app-luatest/msgpack_test.lua @@ -2,6 +2,7 @@ local buffer = require('buffer') local console = require('console') local msgpack = require('msgpack') local ffi = require('ffi') +local fun = require('fun') local t = require('luatest') local g = t.group() @@ -124,7 +125,7 @@ g.test_encode_decode_buffer = function() end g.test_invalid_msgpack = function() - local err = "msgpack.decode: invalid MsgPack" + local err = "Invalid MsgPack - truncated input" -- Invalid msgpack. local first_buffer = {1, 2, 3} @@ -138,6 +139,7 @@ g.test_invalid_msgpack = function() buf.rpos, buf:size() - 1) -- 0xc1 cannot be used in a valid MsgPack. + err = "Invalid MsgPack - illegal code" t.assert_error_msg_content_equals(err, msgpack.decode, '\xc1') t.assert_error_msg_content_equals(err, msgpack.decode, '\x91\xc1') t.assert_error_msg_content_equals(err, msgpack.decode, '\x81\xff\xc1') @@ -245,10 +247,10 @@ g.test_object_from_raw = function() -- invalid msgpack local s = msgpack.encode(o) t.assert_error_msg_content_equals( - "msgpack.object_from_raw: invalid MsgPack", + "Invalid MsgPack - truncated input", msgpack.object_from_raw, s:sub(1, -2)) t.assert_error_msg_content_equals( - "msgpack.object_from_raw: invalid MsgPack", + "Invalid MsgPack - junk after input", msgpack.object_from_raw, s .. s) end @@ -546,3 +548,262 @@ g.test_object_autocomplete = function() local r = tabcomplete('mp:') t.assert_equals(r, {'mp:', 'mp:get(', 'mp:decode(', 'mp:iterator('}) end + +local g_error_details_params = { + decode = { + func = msgpack.decode, + exact = false, + }, + decode_cdata = { + func = function(data) + return msgpack.decode(ffi.cast('char *', data), #data) + end, + exact = false, + }, + object_from_raw = { + func = msgpack.object_from_raw, + exact = true, + }, +} + +local g_error_details = t.group('msgpack.error-details', t.helpers.matrix({ + test = fun.totable(fun.map(function(k) return k end, + g_error_details_params)), +})) + +g_error_details.test_error_details = function(cg) + local params = g_error_details_params[cg.params.test] + local function check_error(data, expected) + local errmsg_prefix = 'Invalid MsgPack - ' + local ok, err = pcall(params.func, data) + t.assert_not(ok) + local actual, prev + while err ~= nil do + local v = err:unpack() + v.prev = nil + v.trace = nil + v.base_type = nil + t.assert_equals(v.type, 'ClientError') + v.type = nil + t.assert_equals(v.code, box.error.INVALID_MSGPACK) + v.code = nil + if v.message:startswith(errmsg_prefix) then + v.message = v.message:sub(#errmsg_prefix + 1) + end + if prev == nil then + actual = v + else + prev.prev = v + end + prev = v + err = err.prev + end + t.assert_equals(actual, expected) + end + check_error('\xc1', { + message = 'illegal code', offset = 0, + }) + check_error('', { + message = 'truncated input', offset = 0, trunc_count = 1, + }) + check_error('\x94\xc0', { + message = 'truncated input', offset = 2, trunc_count = 3, + }) + check_error('\x94\xda\x00', { + message = 'truncated input', offset = 1, trunc_count = 4, + }) + if params.exact then + check_error('\xc0\xc0', { + message = 'junk after input', offset = 1, + }) + check_error('\x92\x91\xc0\xc0\xc1', { + message = 'junk after input', offset = 4, + }) + end + check_error('\xc7\x04\x01\xc0\xc0\xc0\xc0', { + message = 'invalid extension', offset = 3, ext_len = 4, ext_type = 1, + prev = {message = 'cannot unpack decimal'}, + }) + check_error('\xc7\x04\x02\xc0\xc0\xc0\xc0', { + message = 'invalid extension', offset = 3, ext_len = 4, ext_type = 2, + prev = {message = 'cannot unpack uuid'}, + }) + check_error('\xc7\x04\x04\xc0\xc0\xc0\xc0', { + message = 'invalid extension', offset = 3, ext_len = 4, ext_type = 4, + prev = {message = 'cannot unpack datetime'}, + }) + check_error('\xc7\x04\x06\xc0\xc0\xc0\xc0', { + message = 'invalid extension', offset = 3, ext_len = 4, ext_type = 6, + prev = {message = 'cannot unpack interval'}, + }) + check_error('\xc7\x02\x03\x91\xc1', { + message = 'invalid extension', offset = 3, ext_len = 2, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = {message = 'illegal code', offset = 1}, + }, + }) + check_error('\xc7\x02\x03\x92\xc0', { + message = 'invalid extension', offset = 3, ext_len = 2, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = {message = 'truncated input', offset = 2, trunc_count = 1}, + }, + }) + check_error('\xc7\x04\x03\x92\xc0\xc0\xc0', { + message = 'invalid extension', offset = 3, ext_len = 4, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = {message = 'junk after input', offset = 3}, + }, + }) + check_error('\xc7\x01\x03\xc0', { + message = 'invalid extension', offset = 3, ext_len = 1, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = {message = 'error data must be MP_MAP', offset = 0}, + }, + }) + check_error('\xc7\x03\x03\x81\xc0\xc0', { + message = 'invalid extension', offset = 3, ext_len = 3, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = {message = 'error data key must be MP_UINT', offset = 1}, + }, + }) + check_error('\xc7\x01\x03\x80', { + message = 'invalid extension', offset = 3, ext_len = 1, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = {message = 'MP_ERROR_STACK is missing', offset = 1}, + }, + }) + check_error('\xc7\x04\x03\x81\x00\x91\x80', { + message = 'invalid extension', offset = 3, ext_len = 4, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = {message = 'MP_ERROR_TYPE is missing', offset = 4}, + }, + }) + check_error('\xc7\x06\x03\x81\x00\x91\x81\x00\xa0', { + message = 'invalid extension', offset = 3, ext_len = 6, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = {message = 'MP_ERROR_MESSAGE is missing', offset = 6}, + }, + }) + check_error('\xc7\x08\x03\x81\x00\x91\x82\x00\xa0\x03\xa0', { + message = 'invalid extension', offset = 3, ext_len = 8, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = {message = 'MP_ERROR_FILE is missing', offset = 8}, + }, + }) + check_error('\xc7\x03\x03\x81\x00\xc0', { + message = 'invalid extension', offset = 3, ext_len = 3, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = { + message = 'MP_ERROR_STACK value must be MP_ARRAY', + offset = 2, + }, + }, + }) + check_error('\xc7\x04\x03\x81\x00\x91\xc0', { + message = 'invalid extension', offset = 3, ext_len = 4, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = { + message = 'MP_ERROR_STACK array entry must be MP_MAP', + offset = 3, + }, + }, + }) + check_error('\xc7\x06\x03\x81\x00\x91\x81\xc0\xc0', { + message = 'invalid extension', offset = 3, ext_len = 6, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = {message = 'error field key must be MP_UINT', offset = 4}, + }, + }) + check_error('\xc7\x06\x03\x81\x00\x91\x81\x00\xc0', { + message = 'invalid extension', offset = 3, ext_len = 6, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = { + message = 'MP_ERROR_TYPE value must be MP_STR', + offset = 5, + }, + }, + }) + check_error('\xc7\x06\x03\x81\x00\x91\x81\x01\xc0', { + message = 'invalid extension', offset = 3, ext_len = 6, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = { + message = 'MP_ERROR_FILE value must be MP_STR', + offset = 5, + }, + }, + }) + check_error('\xc7\x06\x03\x81\x00\x91\x81\x02\xc0', { + message = 'invalid extension', offset = 3, ext_len = 6, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = { + message = 'MP_ERROR_LINE value must be MP_UINT', + offset = 5, + }, + }, + }) + check_error('\xc7\x06\x03\x81\x00\x91\x81\x03\xc0', { + message = 'invalid extension', offset = 3, ext_len = 6, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = { + message = 'MP_ERROR_MESSAGE value must be MP_STR', + offset = 5, + }, + }, + }) + check_error('\xc7\x06\x03\x81\x00\x91\x81\x04\xc0', { + message = 'invalid extension', offset = 3, ext_len = 6, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = { + message = 'MP_ERROR_ERRNO value must be MP_UINT', + offset = 5, + }, + }, + }) + check_error('\xc7\x06\x03\x81\x00\x91\x81\x05\xc0', { + message = 'invalid extension', offset = 3, ext_len = 6, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = { + message = 'MP_ERROR_CODE value must be MP_UINT', + offset = 5, + }, + }, + }) + check_error('\xc7\x06\x03\x81\x00\x91\x81\x06\xc0', { + message = 'invalid extension', offset = 3, ext_len = 6, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = { + message = 'MP_ERROR_FIELDS value must be MP_MAP', + offset = 5, + }, + }, + }) + check_error('\xc7\x08\x03\x81\x00\x91\x81\x06\x81\xc0\xc0', { + message = 'invalid extension', offset = 3, ext_len = 8, ext_type = 3, + prev = { + message = 'cannot unpack error', + prev = { + message = 'error payload field name must be MP_STR', + offset = 6, + }, + }, + }) +end