fix(api): normalize LuaJSON null sentinels to nil in decoded payloads

KOReader's bundled LuaJSON (json.require) decodes JSON null into a
truthy sentinel function instead of nil. getMetadata responses for a
newly linked book with no server-side progress carry
"progress": null, so the `if not result.progress` guard passed,
indexing the sentinel crashed KOReader outright, and the book could
not be opened ("attempt to index local 'progress' (a function value)"
at the pull handler). "annotations": null would crash
applyServerAnnotations the same way.

Strip the sentinels recursively at the decode boundary in _request so
every call site sees plain nil and existing nil-checks behave as
written. Reproduced in the KOReader flatpak runtime:
json.decode('{"a": null}') yields type(data.a) == "function".

Found while testing a newly OPDS-downloaded book that had no server
progress yet; the crash took down the whole application, not just the
plugin.
This commit is contained in:
John O'Keefe committed 2026-10-12 20:48:00 -04:00
1 parent 75bf511bc1
commit b466af09b0
1 file changed
+18
+18
View File
@@ -1,5 +1,18 @@
local json = require("json")
local json_util = require("json.util")
-- Recursively replace LuaJSON's truthy null-sentinel functions with nil
-- (JSON null), so server payloads behave like normal Lua tables.
local function strip_json_nulls(t)
for k, v in pairs(t) do
if type(v) == "function" then
t[k] = nil
elseif type(v) == "table" then
strip_json_nulls(v)
end
end
return t
end
local logger = require("logger")
local ltn12 = require("ltn12")
local socketutil = require("socketutil")
@@ -86,6 +99,11 @@ function BookhoardAPI:_request(method, path, body, timeouts)
if response_body and #response_body > 0 then
local decode_ok, data = pcall(json.decode, response_body)
if decode_ok and type(data) == "table" then
-- LuaJSON decodes JSON null into a truthy sentinel FUNCTION,
-- which defeats every `if not x.field` nil-check downstream
-- (crash: "attempt to index local 'progress' (a function
-- value)"). Normalize sentinels to nil at the boundary.
strip_json_nulls(data)
return true, data
end
return true, response_body