openclaw - 💡(How to fix) Fix [Bug]: Telegram dispatcher silently swallows send failures via .catch(() => {}); no user-visible reply on network or permanent errors

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Telegram dispatcher silently swallows enqueue failures via .catch(() => {}) and produces no user-visible reply when a send fails (network blip, 429, transient agent failure), leaving the user with no feedback that their message was lost.

Error Message

  1. Observe: the gateway log emits telegram sendMessage failed chat=<id>: <error> and re-throws; the upstream catch in the inbound message handler swallows via .catch(() => {}). No visible Telegram reply is delivered to the user. The user sees nothing.
  • lib/node_modules/openclaw/dist/extensions/telegram/delivery-Dt6oBgpk.js catches send errors, logs telegram sendMessage failed chat=<chatId>: <error>, and re-throws.
  • lib/node_modules/openclaw/dist/extensions/telegram/bot-B7D5mOkP.js (upstream caller in inbound message handler) catches the re-thrown error and either logs-and-swallows or uses the .catch(() => {}) enqueue anti-pattern.

A. 3-class error classifier (new file)

extensions/telegram/src/dispatcher/error-classify.ts: PERMANENT_INPUT = 'permanent_input', // malformed message, parse error PERMANENT_AGENT = 'permanent_agent', // agent crash / config error const msg = (err as Error)?.message ?? String(err); extensions/telegram/src/dispatcher/__tests__/error-classify.test.ts:

Root Cause

  • lib/node_modules/openclaw/dist/extensions/telegram/delivery-Dt6oBgpk.js catches send errors, logs telegram sendMessage failed chat=<chatId>: <error>, and re-throws.
  • lib/node_modules/openclaw/dist/extensions/telegram/bot-B7D5mOkP.js (upstream caller in inbound message handler) catches the re-thrown error and either logs-and-swallows or uses the .catch(() => {}) enqueue anti-pattern.
  • No user-visible Telegram reply is delivered.
  • No host-side detector can close this gap because the silent-swallow path emits no log line at all (we tried).

Fix Action

Fix / Workaround

Telegram dispatcher silently swallows enqueue failures via .catch(() => {}) and produces no user-visible reply when a send fails (network blip, 429, transient agent failure), leaving the user with no feedback that their message was lost.

  1. Configure the Telegram extension and start the gateway: docker compose up -d openclaw-gateway.
  2. Send a DM to the bot that triggers an agent reply.
  3. While the agent is composing the reply, induce a transient dispatcher-side send failure:
    • Block egress to api.telegram.org for ~5 seconds (firewall rule / pfctl / tc qdisc), OR
    • Restart the gateway mid-send: docker compose restart openclaw-gateway.
  4. Observe: the gateway log emits telegram sendMessage failed chat=<id>: <error> and re-throws; the upstream catch in the inbound message handler swallows via .catch(() => {}). No visible Telegram reply is delivered to the user. The user sees nothing.

The dispatcher should also stop silently swallowing the enqueue path via .catch(() => {}) — that anti-pattern emits no log line, so no host-side detector or operator-visibility tool can close the gap.

Code Example

approval-handler.runtime-C56N6mH5.js
channel-DQZFZGBt.js
bot-B7D5mOkP.js
monitor-polling.runtime-CFGCIiVS.js
monitor-webhook.runtime-CJFz745h.js

---

export enum TelegramDispatchErrorClass {
  RETRY_NETWORK = 'retry_network',         // undici fetch failed, DNS timeout, ECONNRESET
  RETRY_RATE_LIMIT = 'retry_rate_limit',   // Telegram 429 retry-after header
  PERMANENT_INPUT = 'permanent_input',     // malformed message, parse error
  PERMANENT_AGENT = 'permanent_agent',     // agent crash / config error
  PERMANENT_AUTH = 'permanent_auth',       // bot token invalid (401/403)
  UNKNOWN = 'unknown',
}

export function classifyTelegramError(err: unknown): TelegramDispatchErrorClass {
  const msg = (err as Error)?.message ?? String(err);
  if (/fetch failed|ECONNRESET|EAI_AGAIN|ENOTFOUND|ETIMEDOUT|AbortError/i.test(msg)) {
    return TelegramDispatchErrorClass.RETRY_NETWORK;
  }
  if (/429|Too Many Requests|retry.after/i.test(msg)) {
    return TelegramDispatchErrorClass.RETRY_RATE_LIMIT;
  }
  if (/401|403|bot token|unauthor/i.test(msg)) {
    return TelegramDispatchErrorClass.PERMANENT_AUTH;
  }
  if (/parse|malformed|invalid.+(message|payload)/i.test(msg)) {
    return TelegramDispatchErrorClass.PERMANENT_INPUT;
  }
  if (/agent.*(crash|uncaught|unavailable)/i.test(msg)) {
    return TelegramDispatchErrorClass.PERMANENT_AGENT;
  }
  return TelegramDispatchErrorClass.UNKNOWN;
}

---

catch (err) {
  const klass = classifyTelegramError(err);
  runtime.log?.(`telegram sendMessage failed chat=${chatId} class=${klass}: ${err.message}`);
  const userMsg = formatUserFacingError(klass, err);
  await tryReplyOrEnqueue(chatId, userMsg);  // best-effort; falls back to delivery-queue on second failure
  throw err;
}
RAW_BUFFERClick to expand / collapse

Bug type: Behavior bug (incorrect output/state without crash)

Beta release blocker: No

Summary

Telegram dispatcher silently swallows enqueue failures via .catch(() => {}) and produces no user-visible reply when a send fails (network blip, 429, transient agent failure), leaving the user with no feedback that their message was lost.

Steps to reproduce

  1. Configure the Telegram extension and start the gateway: docker compose up -d openclaw-gateway.
  2. Send a DM to the bot that triggers an agent reply.
  3. While the agent is composing the reply, induce a transient dispatcher-side send failure:
    • Block egress to api.telegram.org for ~5 seconds (firewall rule / pfctl / tc qdisc), OR
    • Restart the gateway mid-send: docker compose restart openclaw-gateway.
  4. Observe: the gateway log emits telegram sendMessage failed chat=<id>: <error> and re-throws; the upstream catch in the inbound message handler swallows via .catch(() => {}). No visible Telegram reply is delivered to the user. The user sees nothing.

Expected behavior

The user should receive a visible Telegram reply describing the failure class within ~2 seconds:

  • For retry-class errors (RETRY_NETWORK, RETRY_RATE_LIMIT): a short "Retrying due to network…" / "Hitting Telegram rate limits, will retry" message.
  • For permanent-class errors (PERMANENT_INPUT, PERMANENT_AUTH, PERMANENT_AGENT): a short "Command failed: <reason>" message.

The dispatcher should also stop silently swallowing the enqueue path via .catch(() => {}) — that anti-pattern emits no log line, so no host-side detector or operator-visibility tool can close the gap.

Actual behavior

Confirmed against installed [email protected] bundles:

  • lib/node_modules/openclaw/dist/extensions/telegram/delivery-Dt6oBgpk.js catches send errors, logs telegram sendMessage failed chat=<chatId>: <error>, and re-throws.
  • lib/node_modules/openclaw/dist/extensions/telegram/bot-B7D5mOkP.js (upstream caller in inbound message handler) catches the re-thrown error and either logs-and-swallows or uses the .catch(() => {}) enqueue anti-pattern.
  • No user-visible Telegram reply is delivered.
  • No host-side detector can close this gap because the silent-swallow path emits no log line at all (we tried).

Greppable evidence of the anti-pattern in the installed dist (5 files match \.catch\(\(\)\s*=>\s*\{\} in extensions/telegram/):

approval-handler.runtime-C56N6mH5.js
channel-DQZFZGBt.js
bot-B7D5mOkP.js
monitor-polling.runtime-CFGCIiVS.js
monitor-webhook.runtime-CJFz745h.js

OpenClaw version

2026.4.23 (from lib/node_modules/openclaw/package.json in our Docker deployment).

Operating system

macOS 15.x (Darwin 25.3.0); reproducible in the openclaw-gateway Docker container (Linux runtime).

Install method

npm-vendored at lib/node_modules/openclaw/ + docker compose up -d openclaw-gateway (gateway runs in the container; bundles are pre-built from upstream [email protected]).

Model

N/A — this is a dispatcher/transport bug; reproducible across all agent models.


Additional context — proposed fix sketch

We ran a 2026-05-24 investigation (internal "Phase 127") that identified the gap and produced this design sketch. Submitting as a starting point for upstream maintainers, not as a polished PR — we're not familiar with the upstream source conventions and would rather you design the right abstraction than have us drive-by a naïve patch.

A. 3-class error classifier (new file)

extensions/telegram/src/dispatcher/error-classify.ts:

export enum TelegramDispatchErrorClass {
  RETRY_NETWORK = 'retry_network',         // undici fetch failed, DNS timeout, ECONNRESET
  RETRY_RATE_LIMIT = 'retry_rate_limit',   // Telegram 429 retry-after header
  PERMANENT_INPUT = 'permanent_input',     // malformed message, parse error
  PERMANENT_AGENT = 'permanent_agent',     // agent crash / config error
  PERMANENT_AUTH = 'permanent_auth',       // bot token invalid (401/403)
  UNKNOWN = 'unknown',
}

export function classifyTelegramError(err: unknown): TelegramDispatchErrorClass {
  const msg = (err as Error)?.message ?? String(err);
  if (/fetch failed|ECONNRESET|EAI_AGAIN|ENOTFOUND|ETIMEDOUT|AbortError/i.test(msg)) {
    return TelegramDispatchErrorClass.RETRY_NETWORK;
  }
  if (/429|Too Many Requests|retry.after/i.test(msg)) {
    return TelegramDispatchErrorClass.RETRY_RATE_LIMIT;
  }
  if (/401|403|bot token|unauthor/i.test(msg)) {
    return TelegramDispatchErrorClass.PERMANENT_AUTH;
  }
  if (/parse|malformed|invalid.+(message|payload)/i.test(msg)) {
    return TelegramDispatchErrorClass.PERMANENT_INPUT;
  }
  if (/agent.*(crash|uncaught|unavailable)/i.test(msg)) {
    return TelegramDispatchErrorClass.PERMANENT_AGENT;
  }
  return TelegramDispatchErrorClass.UNKNOWN;
}

B. Visible-reply wiring at 3 catch sites

Site 1 — dispatcher send retry chain (compiles to delivery-*.js):

catch (err) {
  const klass = classifyTelegramError(err);
  runtime.log?.(`telegram sendMessage failed chat=${chatId} class=${klass}: ${err.message}`);
  const userMsg = formatUserFacingError(klass, err);
  await tryReplyOrEnqueue(chatId, userMsg);  // best-effort; falls back to delivery-queue on second failure
  throw err;
}

Site 2 — bot inbound message handler (compiles to bot-*.js): same classifyTelegramError + visible-reply pattern.

Site 3 — eliminate the .catch(() => {}) anti-pattern: every silent-swallow in extensions/telegram/src/ becomes an explicit handler that either retries (for RETRY_* classes) or surfaces (for PERMANENT_* classes).

C. Tests

extensions/telegram/src/dispatcher/__tests__/error-classify.test.ts:

  • classifyTelegramError correctly classifies 10 known network shapes
  • dispatcher emits visible reply for each RETRY_* class (3 cases)
  • dispatcher emits visible reply for each PERMANENT_* class (3 cases)
  • Regression guard: greppable \.catch\(\(\)\s*=>\s*\{\} count in extensions/telegram/src/ is 0

D. Why this needs upstream work, not a dist patch

We considered patching lib/node_modules/openclaw/dist/extensions/telegram/*.js directly but rejected it because:

  1. Dist edits don't survive npm install or package upgrades.
  2. Compiled bundle filenames are cache-busted (delivery-Dt6oBgpk.js, bot-B7D5mOkP.js); next build emits different filenames and any host-side hook breaks.
  3. No source-of-truth to be reviewed against.

So it needs to land in the upstream source.

E. Local interim — explicit xfail-strict safety net

While this issue is pending, our deployment carries tests/phase127/test_dm_bot_no_reply_class.py::test_delivery_queue_enqueue_swallow_documented_as_gap (pytest xfail-strict) that documents the gap and fails fast if the silent-swallow paths ever disappear without our knowledge. The day this issue is closed end-to-end, we'll flip that test's marker to a real assertion as a regression guard on our side.


Filed by an OpenClaw operator running 2026.4.23 in a Docker deployment; not a maintainer of openclaw/openclaw. Happy to provide more detail, local repro artifacts, or rebase the sketch against upstream conventions if useful.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

The user should receive a visible Telegram reply describing the failure class within ~2 seconds:

  • For retry-class errors (RETRY_NETWORK, RETRY_RATE_LIMIT): a short "Retrying due to network…" / "Hitting Telegram rate limits, will retry" message.
  • For permanent-class errors (PERMANENT_INPUT, PERMANENT_AUTH, PERMANENT_AGENT): a short "Command failed: <reason>" message.

The dispatcher should also stop silently swallowing the enqueue path via .catch(() => {}) — that anti-pattern emits no log line, so no host-side detector or operator-visibility tool can close the gap.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Bug]: Telegram dispatcher silently swallows send failures via .catch(() => {}); no user-visible reply on network or permanent errors