An OutgoingWebHook configured with method: POST and no inputs never sent a
request — executePost returns early when there is no body to send — but the node’s
recorded error read Webhook POST failed: <url>, indistinguishable from a real
network failure. The reporter spent real time chasing a network problem (rewriting
the receiver, binding a raw socket listener) before finding the early-return in the
processor. Once the node hit ERROR, every later fire — even after the webhook was
correctly reconfigured — was skipped with not invoking node in invalid state ERROR
and never attempted again: for a notification path, a single transient failure
silently disabled the alarm forever, and clearing it required an explicit
update_node setting error: "".
Two independent defects:
ServerWebHookOutboundProcessor.executePost/executePut/executePatch collapsed
every failure mode — “no input configured”, “input node not found”, “the HTTP
call itself failed” — into one generic Webhook <method> failed: <url> message
built by the caller from a Boolean return value, discarding the specific reason
each execute* function had already logged internally.ServerNodeManager.invoke() refuses to dispatch to any node currently in
NodeState.ERROR, uniformly across every node type. That is correct for a
stateful source (a DataPoint/Trigger whose correctness depends on the error
staying visible until explicitly cleared), but an OutgoingWebHook is a
fire-and-forget action with no such invariant — nothing else ever re-invokes it,
so the latch was permanent and silent.WebHookOutboundProcessor.kt: executeGet/Post/Put/Delete/Patch and
handleResponse now return String? (the failure reason, or null on success)
instead of Boolean. POST/PUT/PATCH check meta.inputs.isEmpty() directly and
return a specific reason ("POST needs a body: wire an input node to supply one
(meta.inputs is empty)") before attempting anything; a genuine HTTP failure keeps
a distinct Webhook <method> failed: ... message. process() calls
nodeManager.failed(node, error) only when a reason came back.ServerNodeManager.kt: extracted the invoke-time gate into a pure
blocksInvocation(type: KrillApp, currentState: NodeState): Boolean. DELETING
still blocks every type; ERROR now blocks everything except
KrillApp.Executor.OutgoingWebHook, which is retried on the next invocation
instead of latching. Scoped to this one type deliberately — the codebase’s other
Executor.* members (SMTP, LogicGate, Lambda, Calculation, Compute)
weren’t reported broken and may have different retry-safety properties; widening
the exemption to all of them was not part of what was verified here.process()-style function collapses multiple failure branches into one
boolean before reporting to the caller, check whether the branches are actually
distinguishable to the operator — if the only visibility is a single meta.error
string, that string is the whole diagnostic, not a log-adjacent detail worth
discarding.KrillApp’s Executor.*/Trigger.* nesting (defined in krill-sdk, consumed
here) is Kotlin namespacing only: KrillApp.Executor.OutgoingWebHook extends
KrillApp() directly, not KrillApp.Executor(). is KrillApp.Executor silently
compiles but never matches any nested member — there is no sealed-subtype
relationship to lean on for a “does this belong to the Executor family” check.
Category-based logic over KrillApp needs an explicit enumeration (or a marker
interface on the resolved NodeProcessor), not an is check on the nesting.