From b466af09b0baf421bea5b74142b2380a3e07e442 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Mon, 12 Oct 2026 20:48:00 -0400 Subject: [PATCH] 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. --- BookhoardAPI.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/BookhoardAPI.lua b/BookhoardAPI.lua index 8fc05d7..f818acf 100644 --- a/BookhoardAPI.lua +++ b/BookhoardAPI.lua @@ -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