fix(nodered): parse JSON body in trigger nodes

RED.httpNode.post registers a raw express route with no body parser, so
req.body was undefined and trigger payloads showed all fields null. Add
a zero-dep readJsonBody helper that streams + parses req body.
This commit is contained in:
Mitchell R 2026-05-13 03:07:22 +02:00
parent faaa2cef39
commit 5b380d4694
7 changed files with 49 additions and 12 deletions

25
nodered/src/_http-body.js Normal file
View file

@ -0,0 +1,25 @@
/**
* Tiny JSON body reader for trigger nodes.
*
* RED.httpNode.post(path, handler) registers a raw Express route with no
* body parser, so req.body is undefined. Trigger nodes call readJsonBody(req)
* to get a parsed object (or {} on error / non-JSON).
*
* Zero dependencies avoids relying on Node-RED's bundled body-parser being
* resolvable from our nodesDir.
*/
function readJsonBody(req) {
return new Promise((resolve) => {
if (req.body && typeof req.body === "object") return resolve(req.body);
let data = "";
req.setEncoding("utf8");
req.on("data", (c) => { data += c; });
req.on("end", () => {
if (!data) return resolve({});
try { resolve(JSON.parse(data)); } catch { resolve({}); }
});
req.on("error", () => resolve({}));
});
}
module.exports = { readJsonBody };

View file

@ -14,6 +14,8 @@
* Output msg shape (kept compatible with the previous filter version): * Output msg shape (kept compatible with the previous filter version):
* { topic, kiosk_id, camera_id, source_type, payload } * { topic, kiosk_id, camera_id, source_type, payload }
*/ */
const { readJsonBody } = require("./_http-body.js");
module.exports = function (RED) { module.exports = function (RED) {
// Fixed ingest route. Server-side forwarders that want this node to receive // Fixed ingest route. Server-side forwarders that want this node to receive
// their event should POST to /in/camera.event. (Previous releases used a // their event should POST to /in/camera.event. (Previous releases used a
@ -27,8 +29,8 @@ module.exports = function (RED) {
const filterIdRaw = (config.camera_id || "").toString().trim(); const filterIdRaw = (config.camera_id || "").toString().trim();
const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null; const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null;
function handler(req, res) { async function handler(req, res) {
const body = (req.body && typeof req.body === "object") ? req.body : {}; const body = await readJsonBody(req);
const kioskId = body.kiosk_id !== undefined ? body.kiosk_id const kioskId = body.kiosk_id !== undefined ? body.kiosk_id
: body.source_kiosk_id !== undefined ? body.source_kiosk_id : body.source_kiosk_id !== undefined ? body.source_kiosk_id
: null; : null;

View file

@ -11,6 +11,8 @@
* *
* Output msg.payload: { camera_id, event: "created" | "updated" | "deleted" } * Output msg.payload: { camera_id, event: "created" | "updated" | "deleted" }
*/ */
const { readJsonBody } = require("./_http-body.js");
module.exports = function (RED) { module.exports = function (RED) {
const TOPIC = "camera.changed"; const TOPIC = "camera.changed";
const ROUTE = "/api/internal/" + TOPIC; const ROUTE = "/api/internal/" + TOPIC;
@ -21,8 +23,8 @@ module.exports = function (RED) {
const filterIdRaw = (config.camera_id || "").toString().trim(); const filterIdRaw = (config.camera_id || "").toString().trim();
const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null; const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null;
function handler(req, res) { async function handler(req, res) {
const body = (req.body && typeof req.body === "object") ? req.body : {}; const body = await readJsonBody(req);
const camId = body.camera_id !== undefined ? body.camera_id : null; const camId = body.camera_id !== undefined ? body.camera_id : null;
if (filterId !== null && Number(camId) !== filterId) { if (filterId !== null && Number(camId) !== filterId) {
return res.status(200).end(); return res.status(200).end();

View file

@ -11,6 +11,8 @@
* *
* Output msg.payload: { display_id, kiosk_id, state: "on" | "standby" } * Output msg.payload: { display_id, kiosk_id, state: "on" | "standby" }
*/ */
const { readJsonBody } = require("./_http-body.js");
module.exports = function (RED) { module.exports = function (RED) {
const TOPIC = "display.power.changed"; const TOPIC = "display.power.changed";
const ROUTE = "/api/internal/" + TOPIC; const ROUTE = "/api/internal/" + TOPIC;
@ -21,8 +23,8 @@ module.exports = function (RED) {
const filterIdRaw = (config.display_id || "").toString().trim(); const filterIdRaw = (config.display_id || "").toString().trim();
const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null; const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null;
function handler(req, res) { async function handler(req, res) {
const body = (req.body && typeof req.body === "object") ? req.body : {}; const body = await readJsonBody(req);
const displayId = body.display_id !== undefined ? body.display_id : null; const displayId = body.display_id !== undefined ? body.display_id : null;
if (filterId !== null && Number(displayId) !== filterId) { if (filterId !== null && Number(displayId) !== filterId) {
return res.status(200).end(); return res.status(200).end();

View file

@ -14,6 +14,8 @@
* event: "connected" | "disconnected" | "heartbeat", * event: "connected" | "disconnected" | "heartbeat",
* cpu_temp_c?: number, fan_rpm?: number, fan_pwm?: number } * cpu_temp_c?: number, fan_rpm?: number, fan_pwm?: number }
*/ */
const { readJsonBody } = require("./_http-body.js");
module.exports = function (RED) { module.exports = function (RED) {
const TOPIC = "kiosk.changed"; const TOPIC = "kiosk.changed";
const ROUTE = "/api/internal/" + TOPIC; const ROUTE = "/api/internal/" + TOPIC;
@ -24,8 +26,8 @@ module.exports = function (RED) {
const filterIdRaw = (config.kiosk_id || "").toString().trim(); const filterIdRaw = (config.kiosk_id || "").toString().trim();
const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null; const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null;
function handler(req, res) { async function handler(req, res) {
const body = (req.body && typeof req.body === "object") ? req.body : {}; const body = await readJsonBody(req);
const kioskId = body.kiosk_id !== undefined ? body.kiosk_id : null; const kioskId = body.kiosk_id !== undefined ? body.kiosk_id : null;
if (filterId !== null && Number(kioskId) !== filterId) { if (filterId !== null && Number(kioskId) !== filterId) {
return res.status(200).end(); return res.status(200).end();

View file

@ -10,6 +10,8 @@
* *
* Output msg.payload: { display_id, kiosk_id, layout_id, layout_name } * Output msg.payload: { display_id, kiosk_id, layout_id, layout_name }
*/ */
const { readJsonBody } = require("./_http-body.js");
module.exports = function (RED) { module.exports = function (RED) {
const TOPIC = "layout.changed"; const TOPIC = "layout.changed";
const ROUTE = "/api/internal/" + TOPIC; const ROUTE = "/api/internal/" + TOPIC;
@ -20,8 +22,8 @@ module.exports = function (RED) {
const filterIdRaw = (config.display_id || "").toString().trim(); const filterIdRaw = (config.display_id || "").toString().trim();
const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null; const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null;
function handler(req, res) { async function handler(req, res) {
const body = (req.body && typeof req.body === "object") ? req.body : {}; const body = await readJsonBody(req);
const displayId = body.display_id !== undefined ? body.display_id : null; const displayId = body.display_id !== undefined ? body.display_id : null;
if (filterId !== null && Number(displayId) !== filterId) { if (filterId !== null && Number(displayId) !== filterId) {
return res.status(200).end(); return res.status(200).end();

View file

@ -11,6 +11,8 @@
* Output msg.payload: * Output msg.payload:
* { kiosk_id, kiosk_name, cpu_temp_c, fan_rpm, fan_pwm } * { kiosk_id, kiosk_name, cpu_temp_c, fan_rpm, fan_pwm }
*/ */
const { readJsonBody } = require("./_http-body.js");
module.exports = function (RED) { module.exports = function (RED) {
const TOPIC = "kiosk.status"; const TOPIC = "kiosk.status";
const ROUTE = "/api/internal/" + TOPIC; const ROUTE = "/api/internal/" + TOPIC;
@ -21,8 +23,8 @@ module.exports = function (RED) {
const filterIdRaw = (config.kiosk_id || "").toString().trim(); const filterIdRaw = (config.kiosk_id || "").toString().trim();
const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null; const filterId = filterIdRaw && !isNaN(Number(filterIdRaw)) ? Number(filterIdRaw) : null;
function handler(req, res) { async function handler(req, res) {
const body = (req.body && typeof req.body === "object") ? req.body : {}; const body = await readJsonBody(req);
const kioskId = body.kiosk_id !== undefined ? body.kiosk_id : null; const kioskId = body.kiosk_id !== undefined ? body.kiosk_id : null;
if (filterId !== null && Number(kioskId) !== filterId) { if (filterId !== null && Number(kioskId) !== filterId) {
return res.status(200).end(); return res.status(200).end();