From 61d0e29269a98498f0c45357839f819539b891cc Mon Sep 17 00:00:00 2001 From: GogoVega <92022724+GogoVega@users.noreply.github.com> Date: Sun, 26 Oct 2025 18:46:08 +0100 Subject: [PATCH 1/6] WIP: Allow catch node to catch any error from any node --- .../@node-red/nodes/core/common/25-catch.html | 106 ++++++++++++++---- .../@node-red/nodes/core/common/25-catch.js | 11 +- .../nodes/locales/en-US/messages.json | 5 +- .../@node-red/nodes/locales/fr/messages.json | 11 +- .../@node-red/runtime/lib/flows/Flow.js | 6 + .../@node-red/runtime/lib/nodes/Node.js | 7 +- 6 files changed, 112 insertions(+), 34 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/common/25-catch.html b/packages/node_modules/@node-red/nodes/core/common/25-catch.html index 4b92c9758..23cf54c2a 100644 --- a/packages/node_modules/@node-red/nodes/core/common/25-catch.html +++ b/packages/node_modules/@node-red/nodes/core/common/25-catch.html @@ -12,6 +12,14 @@ +
+ + +
+
+ + +
@@ -30,9 +38,11 @@ category: 'common', color:"#e49191", defaults: { - name: {value:""}, - scope: {value:null, type:"*[]"}, - uncaught: {value:false} + name: { value: "" }, + scope: { value: null, type: "*[]" }, + uncaught: { value: false }, + anyError: { value: false }, + includeConfig: { value: false } }, inputs:0, outputs:1, @@ -82,27 +92,53 @@ }); var dirList = $("#node-input-catch-target-container-div").css({width: "100%", height: "100%"}) .treeList({multi:true}).on("treelistitemmouseover", function(e, item) { - item.node.highlighted = true; - item.node.dirty = true; - RED.view.redraw(); + if (item.node) { + item.node.highlighted = true; + item.node.dirty = true; + RED.view.redraw(); + } }).on("treelistitemmouseout", function(e, item) { - item.node.highlighted = false; - item.node.dirty = true; - RED.view.redraw(); + if (item.node) { + item.node.highlighted = false; + item.node.dirty = true; + RED.view.redraw(); + } }) - var candidateNodes = RED.nodes.filterNodes({z:node.z}); - var allChecked = true; - var items = []; - var nodeItemMap = {}; + + const flowItems = []; + const flowItemMap = {}; + let activeWorkspace = RED.nodes.workspace(node.z); + activeWorkspace = activeWorkspace || RED.nodes.subflow(node.z); + flowItemMap[activeWorkspace.id] = { + id: activeWorkspace.id, + class: "red-ui-palette-header", + label: activeWorkspace.label || activeWorkspace.name || activeWorkspace.id, + expanded: true, + children: [] + }; + flowItemMap.global = { + id: "global", + class: "red-ui-palette-header", + label: this._("catch.label.globalConfig"), + expanded: true, + children: [] + }; + + const nodeItemMap = {}; + const configNodeSet = new Set(); + const candidateNodes = RED.nodes.filterNodes({ z: node.z }); + RED.nodes.eachConfig((cn) => { + // Add config nodes with the same or global scope + if (cn.z === node.z || !cn.z) { + configNodeSet.add(cn); + candidateNodes.push(cn); + } + }); candidateNodes.forEach(function(n) { - if (n.id === node.id) { + if (n.id === node.id || n.id === "global-config") { return; } - var isChecked = scope.indexOf(n.id) !== -1; - - allChecked = allChecked && isChecked; - var nodeDef = RED.nodes.getType(n.type); var label; var sublabel; @@ -119,21 +155,42 @@ if (!nodeDef || !label) { label = n.type; } + const isChecked = scope.indexOf(n.id) !== -1; + const isConfigNode = (nodeDef && nodeDef.category === "config") || (!nodeDef && !n.x && !n.y); nodeItemMap[n.id] = { node: n, label: label, sublabel: sublabel, selected: isChecked, - checkbox: true + checkbox: true, + icon: isConfigNode ? "fa fa-cog" : "" }; - items.push(nodeItemMap[n.id]); + if (!n.z && isConfigNode) { + flowItemMap.global.children.push(nodeItemMap[n.id]); + } else { + flowItemMap[activeWorkspace.id].children.push(nodeItemMap[n.id]); + } + }); + + flowItems.push(flowItemMap[activeWorkspace.id], flowItemMap.global); + + $("#node-input-includeConfig").on("change", function () { + const checked = $(this).prop("checked"); + if (checked) { + dirList.treeList('data', flowItems); + } else { + // Remove config nodes from the list + const filteredItems = flowItemMap[activeWorkspace.id].children + .filter((e) => !configNodeSet.has(e.node)); + dirList.treeList('data', filteredItems); + } }); - dirList.treeList('data',items); $("#node-input-catch-target-select").on("click", function(e) { e.preventDefault(); var preselected = dirList.treeList('selected').map(function(n) {return n.node.id}); RED.tray.hide(); + // TODO: extend selection to config nodes too RED.view.selectNodes({ selected: preselected, onselect: function(selection) { @@ -169,6 +226,13 @@ $(".node-input-target-row").hide(); $(".node-input-uncaught-row").show(); } + if (scope === "group") { + $(".node-input-anyError-row").hide(); + $(".node-input-includeConfig-row").hide(); + } else { + $(".node-input-anyError-row").show(); + $(".node-input-includeConfig-row").show(); + } node._resize(); }); if (this.scope === null) { diff --git a/packages/node_modules/@node-red/nodes/core/common/25-catch.js b/packages/node_modules/@node-red/nodes/core/common/25-catch.js index 5ed525c36..a343451b9 100644 --- a/packages/node_modules/@node-red/nodes/core/common/25-catch.js +++ b/packages/node_modules/@node-red/nodes/core/common/25-catch.js @@ -14,19 +14,20 @@ * limitations under the License. **/ -module.exports = function(RED) { +module.exports = function (RED) { "use strict"; function CatchNode(n) { - RED.nodes.createNode(this,n); - var node = this; + RED.nodes.createNode(this, n); this.scope = n.scope; this.uncaught = n.uncaught; - this.on("input",function(msg, send, done) { + this.anyError = n.anyError; + this.includeConfig = n.includeConfig; + this.on("input", function (msg, send, done) { send(msg); done(); }); } - RED.nodes.registerType("catch",CatchNode); + RED.nodes.registerType("catch", CatchNode); } diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index 5c9c99e75..c19409a40 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -100,7 +100,10 @@ "label": { "source": "Catch errors from", "selectAll": "select all", - "uncaught": "Ignore errors handled by other Catch nodes" + "uncaught": "Ignore errors handled by other Catch nodes", + "anyError": "Allow any error other than message processing", + "includeConfig": "Catch errors from config nodes too", + "globalConfig": "Global config nodes" }, "scope": { "all": "all nodes", diff --git a/packages/node_modules/@node-red/nodes/locales/fr/messages.json b/packages/node_modules/@node-red/nodes/locales/fr/messages.json index e4c2d49ab..e46bf0c77 100644 --- a/packages/node_modules/@node-red/nodes/locales/fr/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/fr/messages.json @@ -98,14 +98,17 @@ "catchNodes": "catch : __number__", "catchUncaught": "catch : non capturé", "label": { - "source": "Détecter les erreurs de", + "source": "Capturer les erreurs ", "selectAll": "Tout sélectionner", - "uncaught": "Ignorer les erreurs gérées par les autres noeuds Catch" + "uncaught": "Ignorer les erreurs gérées par les autres noeuds Catch", + "anyError": "Autoriser toute erreur autre que le traitement des messages", + "includeConfig": "Capturer également les erreurs des noeuds de configuration", + "globalConfig": "Noeuds de configuration globale" }, "scope": { - "all": "tous les noeuds", + "all": "de tous les noeuds", "group": "dans le même groupe", - "selected": "noeuds sélectionnés" + "selected": "des noeuds sélectionnés" } }, "status": { diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 02b976a32..05a245d6c 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -620,9 +620,15 @@ class Flow { handled = node.users[userNode]._flow.handleError(node,logMessage,msg,node.users[userNode]) || handled; } } + } else if (this.id === 'global') { + // TODO: how to trigger Catch nodes? (activeFlows) } else { const candidateNodes = []; this.catchNodes.forEach(targetCatchNode => { + if (msg === null && !targetCatchNode.anyError) { + // Skip if the catch node only accepts errors produced by message processing + return; + } if (targetCatchNode.g && targetCatchNode.scope === 'group' && !reportingNode.g) { // Catch node inside a group, reporting node not in a group - skip it return diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js index 0b1ed349b..4d66c7667 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js @@ -559,9 +559,10 @@ Node.prototype.error = function(logMessage,msg) { if (typeof logMessage != 'boolean') { logMessage = logMessage || ""; } - var handled = false; - if (this._flow && msg && typeof msg === 'object') { - handled = this._flow.handleError(this,logMessage,msg); + let handled = false; + const shouldHandle = (msg && typeof msg === 'object') || msg === undefined; + if (this._flow && shouldHandle) { + handled = this._flow.handleError(this, logMessage, msg || null); } if (!handled) { log_helper(this, Log.ERROR, logMessage); From 9e3f95ecadb084cd0f51f536a1310a401fb5547b Mon Sep 17 00:00:00 2001 From: GogoVega <92022724+GogoVega@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:38:46 +0100 Subject: [PATCH 2/6] Catch errors from config nodes --- .../@node-red/runtime/lib/flows/Flow.js | 18 +++++++++++++++++- .../@node-red/runtime/lib/flows/index.js | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 05a245d6c..09dff8f50 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -26,6 +26,9 @@ let Subflow; let Log; let Group; +/** @type {Record} */ +let activeFlows; + let nodeCloseTimeout = 15000; let asyncMessageDelivery = true; @@ -621,14 +624,24 @@ class Flow { } } } else if (this.id === 'global') { - // TODO: how to trigger Catch nodes? (activeFlows) + for (const flow of Object.values(activeFlows)) { + if (flow.id === 'global') { + continue; + } + handled = flow.handleError(node, logMessage, msg) || handled; + } } else { const candidateNodes = []; + const isConfigNode = !!node._flow.flow.configs[node.id]; this.catchNodes.forEach(targetCatchNode => { if (msg === null && !targetCatchNode.anyError) { // Skip if the catch node only accepts errors produced by message processing return; } + if (isConfigNode && !targetCatchNode.includeConfig) { + // Skip if the catch node only accepts errors produced by non config nodes + return; + } if (targetCatchNode.g && targetCatchNode.scope === 'group' && !reportingNode.g) { // Catch node inside a group, reporting node not in a group - skip it return @@ -859,6 +872,9 @@ module.exports = { Subflow = require("./Subflow"); Group = require("./Group").Group }, + setActiveFlows: function (flows) { + activeFlows = flows; + }, create: function(parent,global,conf) { return new Flow(parent,global,conf) }, diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index f21bd56f9..28a4c03ed 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -70,6 +70,7 @@ function init(runtime) { typeEventRegistered = true; } Flow.init(runtime); + Flow.setActiveFlows(activeFlows); flowUtil.init(runtime); } From cee3fd8395ddf5d491a99720a1d934b579542a3f Mon Sep 17 00:00:00 2001 From: GogoVega <92022724+GogoVega@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:38:59 +0100 Subject: [PATCH 3/6] Temporary fix for unit tests --- test/nodes/core/function/10-function_spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js index 6a04547f4..ba7c22707 100644 --- a/test/nodes/core/function/10-function_spec.js +++ b/test/nodes/core/function/10-function_spec.js @@ -69,7 +69,8 @@ describe('function node', function() { }); it('should do something with the catch node', function(done) { - var flow = [{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);"},{"id":"goodNode","type":"helper"},{"id":"badNode","type":"helper"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]]}]; + // TODO: global flow should not contain these nodes + var flow = [{"id":"flow","type":"tab"},{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);","z":"flow"},{"id":"goodNode","type":"helper","z":"flow"},{"id":"badNode","type":"helper","z":"flow"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]],"anyError":true,"includeConfig":true,"z":"flow"}]; var catchNodeModule = require("nr-test-utils").require("@node-red/nodes/core/common/25-catch.js") helper.load([catchNodeModule, functionNode], flow, function() { var funcNode = helper.getNode("funcNode"); From c2bd5053cfdaa2dcb978dba9fe0952ade5511c03 Mon Sep 17 00:00:00 2001 From: GogoVega <92022724+GogoVega@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:42:40 +0100 Subject: [PATCH 4/6] Fix unit tests --- packages/node_modules/@node-red/runtime/lib/flows/Flow.js | 2 +- test/nodes/core/function/10-function_spec.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 09dff8f50..1a3edc595 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -632,7 +632,7 @@ class Flow { } } else { const candidateNodes = []; - const isConfigNode = !!node._flow.flow.configs[node.id]; + const isConfigNode = !!node._flow?.flow.configs[node.id]; this.catchNodes.forEach(targetCatchNode => { if (msg === null && !targetCatchNode.anyError) { // Skip if the catch node only accepts errors produced by message processing diff --git a/test/nodes/core/function/10-function_spec.js b/test/nodes/core/function/10-function_spec.js index ba7c22707..46159d545 100644 --- a/test/nodes/core/function/10-function_spec.js +++ b/test/nodes/core/function/10-function_spec.js @@ -69,8 +69,7 @@ describe('function node', function() { }); it('should do something with the catch node', function(done) { - // TODO: global flow should not contain these nodes - var flow = [{"id":"flow","type":"tab"},{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);","z":"flow"},{"id":"goodNode","type":"helper","z":"flow"},{"id":"badNode","type":"helper","z":"flow"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]],"anyError":true,"includeConfig":true,"z":"flow"}]; + var flow = [{"id":"flow","type":"tab"},{"id":"funcNode","type":"function","wires":[["goodNode"]],"func":"node.error('This is an error', msg);","x":0,"y":0,"z":"flow"},{"id":"goodNode","type":"helper","z":"flow"},{"id":"badNode","type":"helper","z":"flow"},{"id":"catchNode","type":"catch","scope":null,"uncaught":false,"wires":[["badNode"]],"z":"flow"}]; var catchNodeModule = require("nr-test-utils").require("@node-red/nodes/core/common/25-catch.js") helper.load([catchNodeModule, functionNode], flow, function() { var funcNode = helper.getNode("funcNode"); From 59d8568af0f206794a6604077a45ba60c3a33b34 Mon Sep 17 00:00:00 2001 From: GogoVega <92022724+GogoVega@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:43:33 +0100 Subject: [PATCH 5/6] Highlight config nodes on mouse over --- .../@node-red/nodes/core/common/25-catch.html | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/common/25-catch.html b/packages/node_modules/@node-red/nodes/core/common/25-catch.html index 23cf54c2a..ca4c584c8 100644 --- a/packages/node_modules/@node-red/nodes/core/common/25-catch.html +++ b/packages/node_modules/@node-red/nodes/core/common/25-catch.html @@ -90,18 +90,27 @@ } } }); + const sidebar = $("#red-ui-sidebar-config-category-global"); var dirList = $("#node-input-catch-target-container-div").css({width: "100%", height: "100%"}) .treeList({multi:true}).on("treelistitemmouseover", function(e, item) { if (item.node) { - item.node.highlighted = true; - item.node.dirty = true; - RED.view.redraw(); + if (!item.node.x && !item.node.y) { + sidebar.find(".red-ui-palette-node_id_" + item.node.id).children("div").addClass("highlighted"); + } else { + item.node.highlighted = true; + item.node.dirty = true; + RED.view.redraw(); + } } }).on("treelistitemmouseout", function(e, item) { if (item.node) { - item.node.highlighted = false; - item.node.dirty = true; - RED.view.redraw(); + if (!item.node.x && !item.node.y) { + sidebar.find(".red-ui-palette-node_id_" + item.node.id).children("div").removeClass("highlighted"); + } else { + item.node.highlighted = false; + item.node.dirty = true; + RED.view.redraw(); + } } }) From 5085558cb2dd1f367edf17b91cbf1e61d2bbf799 Mon Sep 17 00:00:00 2001 From: GogoVega <92022724+GogoVega@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:44:06 +0100 Subject: [PATCH 6/6] Ensure global-config is not selectable --- packages/node_modules/@node-red/nodes/core/common/25-catch.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/common/25-catch.html b/packages/node_modules/@node-red/nodes/core/common/25-catch.html index ca4c584c8..2b2f43ca6 100644 --- a/packages/node_modules/@node-red/nodes/core/common/25-catch.html +++ b/packages/node_modules/@node-red/nodes/core/common/25-catch.html @@ -145,7 +145,7 @@ } }); candidateNodes.forEach(function(n) { - if (n.id === node.id || n.id === "global-config") { + if (n.id === node.id || n.type === "global-config") { return; } var nodeDef = RED.nodes.getType(n.type);