Duplicate message on http error?

I’ve struggled today with tracking down a weird event that results in an error that triggers 2 events.

Background: I’m sending some data to a remote API (thingsboard) that is collected around the internet. This node red instance runs in azure docker container and for some reason it will frequently fail http requests (typically with ECONNRESET or ETIMEDOUT). To mitigate this and also make it more robust in general, I have built a system around the http request node that can detect when it fails and handle it by either retry X amount of times after a defined timeout or add it to a backup queue which will resend failed requests in batches at a set interval (5 min). I’ve transitioned away from using retry attempts and instead use backup msg queue.

All of this runs smooth so even if something fails now and then, it eventually succeeds when failed messages are sent again from the backup queue. However, when this is logged, I notice it logs the error happening twice. And it logs data being sent from backup twice. Somewhere I’m getting duplicate messages from the same error! Unfortunately I haven’t found a way to trigger these errors locally, so I can only adjust logging of production instance and wait and hope for error to trigger again and see if I understand more. So far I have not :stuck_out_tongue:

How does HTTP request node behave with these errors? Does it throw an exception? Or does it output the msg as normal and signal the error in statusCode and/or error attributes? At the moment I handle both cases separately and chatgpt suggests this as the source of duplicate error handling:

  1. function node picks up the msg from http node and throws an error if status is not 200. This is caught by catch node A.

  2. Another catch node B listens for HTTP node.

Can’t share the subflow here as it’s too large, but perhaps a screenshot can help:

Experimented some more and added debug log to both Catch http and Catch success, and it seems like both fired at the same time, producing 2 outputs:

2025-09-04 19:05:00 - Start.
2025-09-04 19:05:10 - DEBUG CATCH HTTP:
2025-09-04 19:05:10 - BACKUP: Failed to send data to TB entity weather_station_a (id: '63878154-e4e1-4261-8247-1ddf921f0375') with status code ECONNRESET, adding this attempt to backup to be retried later. Cause: RequestError: read ECONNRESET
2025-09-04 19:05:10 - DEBUG:
2025-09-04 19:05:10 - DEBUG CATCH SUCCESS:
2025-09-04 19:05:10 - BACKUP: Failed to send data to TB entity weather_station_a (id: '63878154-e4e1-4261-8247-1ddf921f0375') with status code ECONNRESET, adding this attempt to backup to be retried later. Cause: Unknown cause.
2025-09-04 19:05:10 - Sent 1 values for weather_station_b.
2025-09-04 19:05:10 - DEBUG:
2025-09-04 19:05:10 - Finished.

That is unless there is another bug which makes duplicate msgs before the http node, which I doubt, but not impossible. Will experiment some more and find a definitive conclusion.

How have you configured the http node and how have you configured the catch nodes (show us screenshots)

The HTTP node is rather empty since it is dynamically used by the subflow:

Catch nodes connected to different nodes.

Catch http:

Catch success:

I’ve done some more testing to verify I wasn’t sending duplicate messages in (which should/could cause duplicate messages out). That’s not the case, I send 1 message in and get 2 out!

Was able to boil it down to a simple recreatable example:

[
    {
        "id": "b4e47790f5dee910",
        "type": "function",
        "z": "5407a20516fa1dd1",
        "name": "set req timeout",
        "func": "msg.requestTimeout = 3000;\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 360,
        "y": 920,
        "wires": [
            [
                "682f9eea81675208"
            ]
        ]
    },
    {
        "id": "0ddb83e5a5f99df3",
        "type": "inject",
        "z": "5407a20516fa1dd1",
        "name": "",
        "props": [],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 190,
        "y": 920,
        "wires": [
            [
                "b4e47790f5dee910"
            ]
        ]
    },
    {
        "id": "682f9eea81675208",
        "type": "http request",
        "z": "5407a20516fa1dd1",
        "name": "",
        "method": "GET",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "https://httpbin.org/delay/10",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": true,
        "headers": [],
        "x": 530,
        "y": 920,
        "wires": [
            [
                "a27cb8b82f1b4f45"
            ]
        ]
    },
    {
        "id": "a27cb8b82f1b4f45",
        "type": "debug",
        "z": "5407a20516fa1dd1",
        "name": "debug output",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 710,
        "y": 920,
        "wires": []
    },
    {
        "id": "bb88539cab14ab66",
        "type": "catch",
        "z": "5407a20516fa1dd1",
        "name": "",
        "scope": null,
        "uncaught": false,
        "x": 500,
        "y": 980,
        "wires": [
            [
                "2548091189cea684"
            ]
        ]
    },
    {
        "id": "2548091189cea684",
        "type": "debug",
        "z": "5407a20516fa1dd1",
        "name": "debug exception",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 720,
        "y": 980,
        "wires": []
    }
]

By using a public test api with 10 sec delay: https://httpbin.org/delay/10 And at the same time set `msg.requestTimeout = 3000; // ms`.

It spits out both output and exception as shown in debug panel. It also spits out error in node: log std out which I’m not sure how to configure.

I could perhaps turn on the “Only send non-2xx responses to Catch node” to collect all of them there at the same place? But it turns out the standard msg output has more details like statusCode which is lost in the exception :cry:

Another solution would be to add a unique id upon entry and then use node context to check if the same error has passed through or not, only allowing the first and dropping subsequent messages with the same id. That’s a lot of hassle though.

Whoever designed this made it a bit of an anti-pattern of what I’m trying to achieve… perhaps the easiest path is to NOT catch errors here? Does the http request node always produce output msg regardless of what happens with the request?

It is difficult to fully understand your goal but cant you simply inhibit the msg going to the function if the HTTP-Request has a non 200 (i.e switch msg.statusCode == 200)

Alternatively, use 1 catch node that catches both & use a rate limiter or flag in the msg to inhibit duplicates?

Alternatively, check msg.error in the msg going into the function and return null to prevent the msg getting processed and throwing a 2nd error.

Alternatively... lots of options.

Are you sure? check the sub properties of the full msg output from the catch node (use a debug set to show full msg)

There is over 10y of history and backwards compatibility to keep in mind. I dont remember the history but you can imagine there are reasons. e.g. the catch nodes were probably invented after many people were already using the HTTP Request node...

1 Like

My goal is robust handling of http requests.

For ENOTFOUND I get it as msg.error.code in the exception. For ETIMEDOUT I get it inside a string in msg.error.source.name :neutral_face:

Here is exception from ETIMEDOUT:

msg = {
  _msgid: "c0b9fef3df8181f6",
  requestTimeout: 3000,
  error: {
    message: "no response from server",
    source: { id: "9b34230ca8533635", type: "http request", name: "http request ETIMEDOUT", count: 1 },
  },
};

Here is exception from ENOTFOUND:

msg = {
  _msgid: "2146477e0291622d",
  requestTimeout: 3000,
  error: {
    message: "RequestError: getaddrinfo ENOTFOUND httpbin-abcdefgegwgwgw.org",
    source: { id: "682f9eea81675208", type: "http request", name: "http request ENOTFOUND", count: 1 },
    code: "ENOTFOUND",
    stack:
      "RequestError: getaddrinfo ENOTFOUND httpbin-abcdefgegwgwgw.org\n    at ClientRequest.<anonymous> (file:///usr/src/node-red/node_modules/got/dist/source/core/index.js:790:107)\n    at Object.onceWrapper (node:events:639:26)\n    at ClientRequest.emit (node:events:536:35)\n    at emitErrorEvent (node:_http_client:104:11)\n    at TLSSocket.socketErrorListener (node:_http_client:518:5)\n    at TLSSocket.emit (node:events:524:28)\n    at emitErrorNT (node:internal/streams/destroy:170:8)\n    at emitErrorCloseNT (node:internal/streams/destroy:129:3)\n    at process.processTicksAndRejections (node:internal/process/task_queues:90:21)\n    at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:120:26)",
  },
};

They seem to have different formats, and I don’t find any documentation on error exception formats.

Which version of node red and nodejs are you using? You flow only provides an output to the Catch node when I run it, there is no output from the node. Running NR 4.1.0 with nodejs 22

2025-09-04 14:46:27 4 Sep 12:46:27 - [info] Node-RED version: v4.1.0
2025-09-04 14:46:27 4 Sep 12:46:27 - [info] Node.js version: v22.13.0
2025-09-04 14:46:27 4 Sep 12:46:27 - [info] Linux 5.15.146.1-microsoft-standard-WSL2 x64 LE
2025-09-04 14:46:27 4 Sep 12:46:27 - [info] Loading palette nodes
2025-09-04 14:46:38 4 Sep 12:46:38 - [info] Dashboard version 3.6.5 started at /ui
2025-09-04 14:46:38 4 Sep 12:46:38 - [info] Settings file : /data/settings.js
2025-09-04 14:46:38 4 Sep 12:46:38 - [info] Context store : 'localFileSystem' [module=localfilesystem]
2025-09-04 14:46:38 4 Sep 12:46:38 - [info] Context store : 'memory' [module=memory]
2025-09-04 14:46:38 4 Sep 12:46:38 - [info] User directory : /data
2025-09-04 14:46:38 4 Sep 12:46:38 - [warn] Projects disabled : editorTheme.projects.enabled=false
2025-09-04 14:46:38 4 Sep 12:46:38 - [info] Flows file : /data/flows.json
2025-09-04 14:46:38 4 Sep 12:46:38 - [info] Server now running at http://127.0.0.1:1880/
2025-09-04 14:46:38 4 Sep 12:46:38 - [info] Starting flows
2025-09-04 14:46:40 4 Sep 12:46:40 - [info] Started flows

Where is that log stdout message that you show coming from?

In the flow you posted you have selected 'Only send non-2xx responses to Catch node'. With that de-selected then I do see both outputs.

As I hinted to earlier:

I also have no idea where it comes from.

Have you got a node called log std out somewhere? Or perhaps that is a Docker thing.

1 Like

Allright yeah, might have copied the code at the sime time as I tried it out :zany_face:

This is the behavior I found so far:

  • By default, http request node outputs both msg and throws exception for some/all errors.
  • If selecting “Only send non-2xx…” it doesn’t send output msg on some/all errors.
  • Exception format is different from normal and also different depending on the type of error (for example may have code attribute for error / status code, or hide it in a string in some other attribute.

I think my best way forward is to delete the catch and ignore exceptions of the http request node. That way I can control the message flow and hopefully get more details in a structured way that is easier to predict.

Sorry the log std out is something different, accidentally tested this in a flow which already had some catch node and logging set up.

This raises another concern. I should perhaps have a catch node which discards the exceptions, to avoid other catch nodes (in parent subflows and/or flows) to accidentally catch errors from http request node? But then I need to always check the “Ignore errors handled by other Catch nodes”.

With 'Only send non-2xx...` selected, do 2xx response errors get sent to the Catch node?

I didn’t try with numerical error codes (for example 400), but the “word-errors” are thrown to catch yes. Regardless, the exception format is unpredictable to me so I’d rather just throw the error myself.