Du hast eine Zabbix → n8n → Claude AI Remediation-Pipeline gebaut. Zabbix erkennt ein Problem, feuert einen Webhook an n8n, n8n lässt Claude eine Diagnose und automatische Reparatur ausführen, und bei Fehlschlag geht eine Eskalation per Telegram raus.
Klingt robust. War es nicht.
10 Tage lang. 264+ Alerts. Null Remediation. Null Eskalation. Null Fehler in der n8n-UI.
TL;DR
Drei Bugs haben sich gegenseitig gedeckt:
- Container-Port vs Host-Port: Interner HTTP-Call ging an
localhost:8098(Host-Port) stattlocalhost:5678(Container-Port). ECONNREFUSED. $json-Kontext geht verloren: Nach dem fehlgeschlagenen HTTP-Request enthielt$jsondie Fehler-Response statt der Original-Daten. Downstream-Nodes bekamen leere Werte.- Stille Kaskade: Die Telegram-Eskalation (Fallback für genau diesen Fall) schlug ebenfalls fehl — weil
chat_idaus dem verlorenen$json-Kontext kam.
Ergebnis: Alerts wurden in der Datenbank als “pending” markiert und vergessen. Kein Mensch wurde informiert. Kein System wurde repariert.
Ursache 1: Container-Port vs Host-Port
Der Alert Router Workflow in n8n hat per HTTP Request Node den Remediation-Workflow aufgerufen:
http://localhost:8098/webhook/zabbix-remediate
Das Problem: 8098 ist der Host-seitige Docker-Port-Mapping. Die Docker-Compose sieht so aus:
services:
n8n:
ports:
- "8098:5678"
Vom Host aus erreichst du n8n über Port 8098 — korrekt. Aber der HTTP Request Node läuft innerhalb des n8n-Containers. Und innerhalb des Containers lauscht n8n auf Port 5678.
localhost:8098 innerhalb des Containers = ECONNREFUSED. Nichts hört dort zu.
Die korrekte URL:
http://localhost:5678/webhook/zabbix-remediate
Warum hat das niemand bemerkt? Beide beteiligten Nodes hatten continueOnFail: true gesetzt. Der ECONNREFUSED wurde geschluckt wie ein Hustenbonbon. Der Workflow lief “erfolgreich” weiter — nur eben mit einer Fehler-Response statt echter Daten.
Ursache 2: $json-Kontext geht verloren
In n8n bezieht sich $json immer auf den Output des direkt vorherigen Nodes. Das klingt offensichtlich, ist es aber nicht — besonders wenn continueOnFail: true aktiv ist.
Der normale Flow:
Parse Alert → HTTP Request → Process Response → Telegram
Wenn der HTTP Request Node fehlschlägt (ECONNREFUSED), aber dank continueOnFail weiterläuft, enthält sein Output nicht die Alert-Daten, sondern die Fehler-Response:
{
"error": "ECONNREFUSED",
"message": "connect ECONNREFUSED 127.0.0.1:8098"
}
Jeder nachfolgende Node, der $json.host oder $json.chat_id referenziert, bekommt: nichts. Leerer String. undefined.
Der Fix
Statt $json.host (= Output des letzten Nodes) explizit den gewünschten Upstream-Node referenzieren:
// Schlecht — fragil, bricht bei Fehlern
$json.host
// Gut — referenziert direkt den Parse-Node
$('Parse Alert').first().json.host
$('NodeName').first().json.* greift immer auf den Output des benannten Nodes zu, egal was dazwischen passiert. Das ist der Unterschied zwischen “funktioniert wenn alles gut geht” und “funktioniert”.
Ursache 3: Die stille Kaskade
Jetzt kommt der Teil, der wirklich wehtut.
Die Pipeline hatte eine Telegram-Eskalation als Fallback. Wenn die automatische Remediation fehlschlägt, soll eine Nachricht an den Admin-Chat gehen. Das war der Sicherheitsmechanismus.
Der Telegram-Node hat die chat_id aus $json.chat_id gelesen. Und wie wir gerade gelernt haben: $json enthielt zu diesem Zeitpunkt die Fehler-Response vom HTTP Request. Keine chat_id. Leerer Wert.
Telegram-API mit leerer chat_id: Fehler. Aber — du ahnst es — auch der Telegram-Node hatte continueOnFail: true.
Also:
- Remediation schlägt fehl (ECONNREFUSED) → wird geschluckt
- Telegram-Eskalation schlägt fehl (leere chat_id) → wird geschluckt
- Alert wird in der Datenbank als “pending” markiert
- Niemand guckt in die “pending”-Queue
Drei Sicherheitsebenen, alle drei ausgehebelt. Nicht durch einen Angriff, nicht durch einen Ausfall — durch continueOnFail: true.
Lösung
1. Container-internen Port verwenden
http://localhost:5678/webhook/zabbix-remediate
Faustregel: Wenn ein Container sich selbst aufruft, immer den internen Port nutzen. Der Host-Port existiert nur außerhalb des Containers.
2. Explizite Node-Referenzen statt $json
Jeder Node, der Daten von weiter upstream braucht, referenziert den Quell-Node direkt:
// Alert-Daten
const host = $('Parse Alert').first().json.host;
const severity = $('Parse Alert').first().json.severity;
// Chat-ID
const chatId = $('Parse Alert').first().json.chat_id;
3. continueOnFail nur mit Error Handling
continueOnFail: true ist nicht grundsätzlich schlecht — aber nur, wenn du den Fehler auch behandelst. Das bedeutet: nach dem Node eine explizite Prüfung einbauen.
In n8n kannst du das mit einem IF-Node machen:
// Prüfe ob der HTTP Request erfolgreich war
$json.error === undefined
Oder besser: n8n’s eingebautes Error Handling nutzen. Seit n8n 1.x gibt es Error Trigger Workflows — ein separater Workflow, der bei jedem Fehler in jedem Workflow ausgeführt wird.
4. Dead Letter Queue statt “pending forever”
Alerts die nach 3 Versuchen immer noch “pending” sind, sollten eskaliert werden — über einen unabhängigen Kanal. Nicht über denselben Workflow der gerade fehlschlägt.
Cron (alle 15 Min) → DB Query "pending > 30 Min" → Direct Telegram API Call (hardcoded chat_id)
Hardcoded chat_id. Kein $json. Keine Abhängigkeit von Upstream-Daten. Wenn dieser Alert fehlschlägt, hast du ein anderes Problem.
Verifikation
Nach dem Fix, so prüfst du ob alles funktioniert:
Port-Test im Container
# Aus dem Container heraus testen
docker exec n8n wget -qO- http://localhost:5678/healthz
# Webhook erreichbar?
docker exec n8n wget -qO- --post-data='{"test":true}' \
http://localhost:5678/webhook-test/zabbix-remediate
Error Path testen
Nicht nur den Happy Path testen. Provoziere Fehler:
# Sende einen Alert mit ungültigen Daten
curl -X POST http://localhost:8098/webhook/zabbix-alert \
-H "Content-Type: application/json" \
-d '{"host": "", "severity": "invalid", "chat_id": ""}'
Wenn jetzt keine Telegram-Nachricht kommt, hast du das gleiche Problem nochmal.
n8n Execution Log prüfen
# Die letzten fehlgeschlagenen Executions
docker exec n8n n8n list:workflow --active
Geh in die n8n-UI unter Executions und filtere nach “Error”. Wenn dort nichts steht, aber Alerts reinkommen — dann schluckt continueOnFail immer noch Fehler.
Fazit
continueOnFail: true ist der || true der Workflow-Automation. Es macht deine Pipeline nicht robuster — es macht sie still. Fehler verschwinden nicht, sie werden unsichtbar.
Die drei Lektionen:
Container-interne Calls nutzen den internen Port. Nicht den Host-Port. Klingt trivial, ist aber ein Klassiker wenn man Workflows in der UI baut und mit Host-URLs testet.
$jsonist flüchtig. Sobald ein Node fehlschlägt (auch “erfolgreich” fehlschlägt dankcontinueOnFail), enthält$jsonden Fehler-Output. Nutze$('NodeName').first().json.*für alles was mehr als einen Hop entfernt ist.Teste den Error Path. Dein Fallback ist nur so gut wie sein letzter Test. Wenn der Fallback dieselben Fehlerquellen hat wie der Primary Path, hast du keinen Fallback — du hast zwei Wege die gleichzeitig ausfallen.
264 Alerts in 10 Tagen. Null Alarme. Die Pipeline hat funktioniert — nur nicht so, wie gedacht.