diff --git a/changelog/unreleased/kong/pdk-json-encoding-numbers-precision.yml b/changelog/unreleased/kong/pdk-json-encoding-numbers-precision.yml new file mode 100644 index 000000000000..0560d8b68150 --- /dev/null +++ b/changelog/unreleased/kong/pdk-json-encoding-numbers-precision.yml @@ -0,0 +1,3 @@ +message: Increase the precision of JSON number encoding from 14 to 16 decimals +type: feature +scope: PDK diff --git a/kong-3.6.0-0.rockspec b/kong-3.6.0-0.rockspec index 3b0e10e449db..5e9ec6846656 100644 --- a/kong-3.6.0-0.rockspec +++ b/kong-3.6.0-0.rockspec @@ -180,6 +180,7 @@ build = { ["kong.tools.module"] = "kong/tools/module.lua", ["kong.tools.ip"] = "kong/tools/ip.lua", ["kong.tools.http"] = "kong/tools/http.lua", + ["kong.tools.cjson"] = "kong/tools/cjson.lua", ["kong.runloop.handler"] = "kong/runloop/handler.lua", ["kong.runloop.events"] = "kong/runloop/events.lua", diff --git a/kong/constants.lua b/kong/constants.lua index fc3b8a18a3b2..649a4380d6e1 100644 --- a/kong/constants.lua +++ b/kong/constants.lua @@ -92,6 +92,7 @@ for k in pairs(key_formats_map) do end local constants = { + CJSON_MAX_PRECISION = 16, BUNDLED_PLUGINS = plugin_map, DEPRECATED_PLUGINS = deprecated_plugin_map, BUNDLED_VAULTS = vault_map, diff --git a/kong/db/strategies/postgres/init.lua b/kong/db/strategies/postgres/init.lua index 74da93465aa6..c09bf9ed5878 100644 --- a/kong/db/strategies/postgres/init.lua +++ b/kong/db/strategies/postgres/init.lua @@ -1,6 +1,5 @@ local arrays = require "pgmoon.arrays" local json = require "pgmoon.json" -local cjson = require "cjson" local cjson_safe = require "cjson.safe" local utils = require "kong.tools.utils" local new_tab = require "table.new" @@ -180,6 +179,10 @@ local function escape_literal(connector, literal, field) return concat { "TO_TIMESTAMP(", connector:escape_literal(tonumber(fmt("%.3f", literal))), ") AT TIME ZONE 'UTC'" } end + if field.type == "integer" then + return fmt("%16.f", literal) + end + if field.type == "array" or field.type == "set" then if not literal[1] then return connector:escape_literal("{}") @@ -211,7 +214,7 @@ local function escape_literal(connector, literal, field) elseif et == "map" or et == "record" or et == "json" then local jsons = {} for i, v in ipairs(literal) do - jsons[i] = cjson.encode(v) + jsons[i] = cjson_safe.encode(v) end return encode_array(jsons) .. '::JSONB[]' @@ -522,7 +525,7 @@ local function page(self, size, token, foreign_key, foreign_entity_name, options insert(offset, row[field_name]) end - offset = cjson.encode(offset) + offset = cjson_safe.encode(offset) offset = encode_base64(offset, true) return rows, nil, offset diff --git a/kong/db/strategies/postgres/tags.lua b/kong/db/strategies/postgres/tags.lua index 48341af94f63..f9b8bb884458 100644 --- a/kong/db/strategies/postgres/tags.lua +++ b/kong/db/strategies/postgres/tags.lua @@ -1,4 +1,3 @@ -local cjson = require "cjson" local cjson_safe = require "cjson.safe" @@ -118,7 +117,7 @@ local function page(self, size, token, options, tag) last_ordinality } - offset = cjson.encode(offset) + offset = cjson_safe.encode(offset) offset = encode_base64(offset, true) return rows, nil, offset diff --git a/kong/globalpatches.lua b/kong/globalpatches.lua index c3782f0c8a0f..332e07db5903 100644 --- a/kong/globalpatches.lua +++ b/kong/globalpatches.lua @@ -1,3 +1,5 @@ +local constants = require "kong.constants" + local ran_before @@ -15,15 +17,16 @@ return function(options) local meta = require "kong.meta" - local cjson = require("cjson.safe") - cjson.encode_sparse_array(nil, nil, 2^15) + local cjson_safe = require("cjson.safe") + cjson_safe.encode_sparse_array(nil, nil, 2^15) + cjson_safe.encode_number_precision(constants.CJSON_MAX_PRECISION) local pb = require "pb" -- let pb decode arrays to table cjson.empty_array_mt metatable -- so empty arrays are encoded as `[]` instead of `nil` or `{}` by cjson. pb.option("decode_default_array") - pb.defaults("*array", cjson.empty_array_mt) + pb.defaults("*array", cjson_safe.empty_array_mt) if options.cli then -- disable the _G write guard alert log introduced in OpenResty 1.15.8.1 diff --git a/kong/pdk/request.lua b/kong/pdk/request.lua index 06fb846a2ae6..10bb08dfe5df 100644 --- a/kong/pdk/request.lua +++ b/kong/pdk/request.lua @@ -6,7 +6,7 @@ -- @module kong.request -local cjson = require "cjson.safe".new() +local cjson = require "kong.tools.cjson" local multipart = require "multipart" local phase_checker = require "kong.pdk.private.phases" local normalize = require("kong.tools.uri").normalize @@ -44,8 +44,6 @@ local decode_args = ngx.decode_args local PHASES = phase_checker.phases -cjson.decode_array_with_array_mt(true) - local function new(self) local _REQUEST = {} @@ -832,7 +830,7 @@ local function new(self) return nil, err, CONTENT_TYPE_JSON end - local json = cjson.decode(body) + local json = cjson.decode_with_array_mt(body) if type(json) ~= "table" then return nil, "invalid json body", CONTENT_TYPE_JSON end diff --git a/kong/pdk/service/response.lua b/kong/pdk/service/response.lua index ec51fe4fac08..5a6621abf543 100644 --- a/kong/pdk/service/response.lua +++ b/kong/pdk/service/response.lua @@ -3,7 +3,7 @@ -- @module kong.service.response -local cjson = require "cjson.safe".new() +local cjson = require "kong.tools.cjson" local multipart = require "multipart" local phase_checker = require "kong.pdk.private.phases" local string_tools = require "kong.tools.string" @@ -23,8 +23,6 @@ local setmetatable = setmetatable local check_phase = phase_checker.check -cjson.decode_array_with_array_mt(true) - local replace_dashes = string_tools.replace_dashes local replace_dashes_lower = string_tools.replace_dashes_lower @@ -356,7 +354,7 @@ local function new(pdk, major_version) elseif find(content_type_lower, CONTENT_TYPE_JSON, 1, true) == 1 then local body = response.get_raw_body() - local json = cjson.decode(body) + local json = cjson.decode_with_array_mt(body) if type(json) ~= "table" then return nil, "invalid json body", CONTENT_TYPE_JSON end diff --git a/kong/tools/cjson.lua b/kong/tools/cjson.lua new file mode 100644 index 000000000000..ea668be90178 --- /dev/null +++ b/kong/tools/cjson.lua @@ -0,0 +1,21 @@ +local cjson = require "cjson.safe".new() +local constants = require "kong.constants" + +cjson.decode_array_with_array_mt(true) +cjson.encode_sparse_array(nil, nil, 2^15) +cjson.encode_number_precision(constants.CJSON_MAX_PRECISION) + +local _M = {} + + +function _M.encode(json_text) + return cjson.encode(json_text) +end + +function _M.decode_with_array_mt(json_text) + return cjson.decode(json_text) +end + +_M.array_mt = cjson.array_mt + +return _M diff --git a/spec/02-integration/04-admin_api/25-max_safe_integer_spec.lua b/spec/02-integration/04-admin_api/25-max_safe_integer_spec.lua new file mode 100644 index 000000000000..a54ff9452258 --- /dev/null +++ b/spec/02-integration/04-admin_api/25-max_safe_integer_spec.lua @@ -0,0 +1,110 @@ +local helpers = require "spec.helpers" + +local LMDB_MAP_SIZE = "10m" + +for _, strategy in helpers.each_strategy() do + if strategy ~= "off" then + describe("Admin API #" .. strategy, function() + local bp + local client, route + + lazy_setup(function() + bp = helpers.get_db_utils(strategy, { + "routes", + "services", + }) + + route = bp.routes:insert({ + paths = { "/route_with_max_safe_integer_priority"}, + regex_priority = 9007199254740992, + }) + + assert(helpers.start_kong({ + database = strategy, + })) + end) + + lazy_teardown(function() + helpers.stop_kong(nil, true) + end) + + before_each(function() + client = assert(helpers.admin_client()) + end) + + after_each(function() + if client then + client:close() + end + end) + + it("the maximum safe integer can be accurately represented as a decimal number", function() + local res = assert(client:send { + method = "GET", + path = "/routes/" .. route.id + }) + assert.res_status(200, res) + assert.match_re(res:read_body(), "9007199254740992") + end) + end) + end + + if strategy == "off" then + describe("Admin API #off", function() + local client + + lazy_setup(function() + assert(helpers.start_kong({ + database = "off", + lmdb_map_size = LMDB_MAP_SIZE, + stream_listen = "127.0.0.1:9011", + nginx_conf = "spec/fixtures/custom_nginx.template", + })) + end) + + lazy_teardown(function() + helpers.stop_kong(nil, true) + end) + + before_each(function() + client = assert(helpers.admin_client()) + end) + + after_each(function() + if client then + client:close() + end + end) + + it("the maximum safe integer can be accurately represented as a decimal number", function() + local res = assert(client:send { + method = "POST", + path = "/config", + body = { + config = [[ + _format_version: "1.1" + services: + - name: my-service + id: 0855b320-0dd2-547d-891d-601e9b38647f + url: https://localhost + routes: + - name: my-route + id: 481a9539-f49c-51b6-b2e2-fe99ee68866c + paths: + - / + regex_priority: 9007199254740992 + ]], + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + + assert.response(res).has.status(201) + local res = client:get("/routes/481a9539-f49c-51b6-b2e2-fe99ee68866c") + assert.res_status(200, res) + assert.match_re(res:read_body(), "9007199254740992") + end) + end) + end +end diff --git a/t/01-pdk/08-response/11-exit.t b/t/01-pdk/08-response/11-exit.t index f45564eed560..4a6f7a624c92 100644 --- a/t/01-pdk/08-response/11-exit.t +++ b/t/01-pdk/08-response/11-exit.t @@ -1155,3 +1155,28 @@ X-test: test manually setting Transfer-Encoding. Ignored. + +=== TEST 45: response.exit() json encoding of numbers with a precision of 16 decimals +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + default_type 'text/test'; + access_by_lua_block { + require("kong.globalpatches")() + local PDK = require "kong.pdk" + local pdk = PDK.new() + + pdk.response.exit(200, { n = 9007199254740992 }) + } + } +--- request +GET /t +--- error_code: 200 +--- response_headers_like +Content-Type: application/json; charset=utf-8 +--- response_body chop +{"n":9007199254740992} +--- no_error_log +[error] + +