Skip to content

fix(telegram): fallback to alternative API IP when DNS-resolved endpoint is unreachable#48812

Closed
Cypherm wants to merge 1 commit intoopenclaw:mainfrom
Cypherm:fix/telegram-ip-fallback
Closed

fix(telegram): fallback to alternative API IP when DNS-resolved endpoint is unreachable#48812
Cypherm wants to merge 1 commit intoopenclaw:mainfrom
Cypherm:fix/telegram-ip-fallback

Conversation

@Cypherm
Copy link
Copy Markdown
Contributor

@Cypherm Cypherm commented Mar 17, 2026

Summary

When api.telegram.org DNS resolves to an IP that is unreachable from certain ISPs (confirmed: Taiwan/HiNet), the bot enters a permanent failure loop — default fetch times out, IPv4-only retry also times out (same bad IP), polling restarts, repeat forever.

This PR adds a third fallback tier to the existing retry chain in resolveTelegramTransport():

  1. Default dispatcher (system DNS, dual-stack) → ETIMEDOUT
  2. IPv4-only dispatcher (system DNS, forced IPv4) → ETIMEDOUT
  3. Fallback IP dispatcher (DNS pinning to known working Telegram Bot API IP via custom connect.lookup) → success

The fallback uses the same technique as curl --resolve: connects to an alternative IP (149.154.167.220) while TLS validates against api.telegram.org via SNI. This is fully transparent — no config changes, no user intervention.

Key properties

  • Zero regression risk: fallback only activates on ETIMEDOUT/ENETUNREACH (failure path)
  • Sticky: once armed, subsequent requests skip directly to the working dispatcher
  • Per-resolver scoped: follows the same closure-based architecture introduced in fix(telegram): move network fallback to resolver-scoped dispatchers #40740
  • Proxy-safe: fallback IP dispatcher respects NO_PROXY bypass and env proxy settings

Verification

Tested by simulating DNS failure via /etc/hosts pointing api.telegram.org to an unreachable IP (192.0.2.1):

1. Default fetch (simulates current behavior)...
   TIMEOUT — reproduces the bug

2. Fallback IP dispatcher (this fix)...
   HTTP 200 — auto-recovered via 149.154.167.220

Also verified that all three DNS resolvers (system, Google 8.8.8.8, Cloudflare 1.1.1.1) return the same IP for api.telegram.org, confirming that DNS-level fallback would not help — direct IP pinning is the correct approach.

Relates to #45372, #28835

Test plan

  • Existing fetch.test.ts tests pass (18 pre-existing failures unrelated to this change, 2 passing — confirmed identical with and without patch)
  • Format check passes (pnpm format:check)
  • TypeScript check passes (pnpm tsgo — no new errors)
  • Manual verification: /etc/hosts override to unreachable IP → fallback recovers automatically

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 [email protected]

@openclaw-barnacle openclaw-barnacle bot added channel: telegram Channel integration: telegram size: S labels Mar 17, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR adds a third tier to the existing Telegram transport retry chain in resolveTelegramTransport() to handle cases where the DNS-resolved IP for api.telegram.org is unreachable from certain ISPs (e.g. Taiwan/HiNet). The fix pins connections to a known-good fallback IP (149.154.167.220) via a custom lookup function while preserving TLS SNI for api.telegram.org, following the same closure/sticky-state architecture as the existing IPv4 fallback.

  • The retry escalation logic (default → IPv4-only → fallback IP) is well-structured and correctly gates on allowStickyIpv4Fallback, preserving proxy and caller-dispatcher semantics.
  • TELEGRAM_FALLBACK_IPS is hardcoded with a single IPv4 address and no update or rotation mechanism — if Telegram ever retires or reassigns this IP the fallback will silently fail.
  • createFallbackIpLookup hardcodes family: 4 for all returned addresses regardless of the actual IP format; this is correct today but fragile if IPv6 fallback addresses are added later.
  • In resolveFallbackIpDispatcher, connect and proxyTls are set to the same object reference for the EnvHttpProxyAgent branch, which diverges from the rest of the codebase's defensive copy pattern.

Confidence Score: 4/5

  • Safe to merge — the fallback only activates on confirmed failure paths and the core logic is sound; the noted issues are maintainability concerns rather than correctness bugs.
  • The new retry tier correctly follows the existing sticky-fallback architecture, is guarded behind the established allowStickyIpv4Fallback / error-code checks, and is scoped only to the failure path. The three flagged issues (hardcoded IP, family always 4, shared object reference) are style/maintainability concerns and do not affect correctness under the current configuration.
  • extensions/telegram/src/fetch.ts — specifically the hardcoded fallback IP and the shared connect/proxyTls reference in the env-proxy branch.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/telegram/src/fetch.ts
Line: 49

Comment:
**Hardcoded fallback IP with no update mechanism**

`149.154.167.220` is hardcoded as the only fallback IP with no rotation, staleness detection, or easy update path. If Telegram retires or reassigns this IP (even if stable today), the fallback tier will silently fail for all users with the bad DNS routing — which is precisely the scenario this PR is designed to fix.

Consider at minimum adding a comment documenting how to verify and update this IP (e.g. `dig api.telegram.org` or `curl -sv https://api.telegram.org`), or expanding the array to include the full set of known Telegram Bot API IPs (Telegram publicly documents ranges `149.154.167.0/22` and `91.108.4.0/22`). Having more than one IP increases resilience and makes future rotation less urgent.

```suggestion
const TELEGRAM_FALLBACK_IPS: readonly string[] = [
  "149.154.167.220", // primary fallback – verify periodically with `dig api.telegram.org`
];
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/telegram/src/fetch.ts
Line: 144-149

Comment:
**`family` is hardcoded to `4` for all fallback addresses**

All entries in `fallbackIps` are reported as `family: 4` regardless of their actual format. This is correct for the current single IPv4 entry, but if an IPv6 address is ever added to `TELEGRAM_FALLBACK_IPS` the lookup would advertise an IPv6 address as family 4, which undici would likely reject or misroute.

Consider detecting the family dynamically using `net.isIPv4` / `net.isIPv6`:

```suggestion
    if (opts && "all" in opts && opts.all) {
      (callback as (err: null, addresses: dns.LookupAddress[]) => void)(
        null,
        fallbackIps.map((addr) => ({
          address: addr,
          family: net.isIPv4(addr) ? 4 : 6,
        })),
      );
    } else {
      (callback as (err: null, address: string, family: number) => void)(
        null,
        ip,
        net.isIPv4(ip) ? 4 : 6,
      );
    }
```

(Requires `import * as net from "node:net"` at the top of the file.)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/telegram/src/fetch.ts
Line: 527-529

Comment:
**`connect` and `proxyTls` share the same object reference**

The established pattern throughout this file (see `resolveTelegramDispatcherPolicy` and `createTelegramDispatcher`) always passes independent copies of the connect options to `connect` and `proxyTls`:

```ts
connect: { ...connect }, proxyTls: { ...connect }
```

Here, both fields receive the same object reference. If `EnvHttpProxyAgent` ever mutates the options object during construction or request dispatch (e.g., to normalise or cache a field), mutations through one reference would silently affect the other. Align with the existing pattern:

```suggestion
      fallbackIpDispatcher = stickyShouldUseEnvProxy
        ? new EnvHttpProxyAgent({ connect: { ...connect }, proxyTls: { ...connect } })
        : new Agent({ connect });
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 29a3d59

* Used as a last-resort fallback when the DNS-resolved IP is unreachable.
* Connected via DNS pinning (custom lookup) so TLS validates against api.telegram.org.
*/
const TELEGRAM_FALLBACK_IPS: readonly string[] = ["149.154.167.220"];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded fallback IP with no update mechanism

149.154.167.220 is hardcoded as the only fallback IP with no rotation, staleness detection, or easy update path. If Telegram retires or reassigns this IP (even if stable today), the fallback tier will silently fail for all users with the bad DNS routing — which is precisely the scenario this PR is designed to fix.

Consider at minimum adding a comment documenting how to verify and update this IP (e.g. dig api.telegram.org or curl -sv https://api.telegram.org), or expanding the array to include the full set of known Telegram Bot API IPs (Telegram publicly documents ranges 149.154.167.0/22 and 91.108.4.0/22). Having more than one IP increases resilience and makes future rotation less urgent.

Suggested change
const TELEGRAM_FALLBACK_IPS: readonly string[] = ["149.154.167.220"];
const TELEGRAM_FALLBACK_IPS: readonly string[] = [
"149.154.167.220", // primary fallback – verify periodically with `dig api.telegram.org`
];
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/telegram/src/fetch.ts
Line: 49

Comment:
**Hardcoded fallback IP with no update mechanism**

`149.154.167.220` is hardcoded as the only fallback IP with no rotation, staleness detection, or easy update path. If Telegram retires or reassigns this IP (even if stable today), the fallback tier will silently fail for all users with the bad DNS routing — which is precisely the scenario this PR is designed to fix.

Consider at minimum adding a comment documenting how to verify and update this IP (e.g. `dig api.telegram.org` or `curl -sv https://api.telegram.org`), or expanding the array to include the full set of known Telegram Bot API IPs (Telegram publicly documents ranges `149.154.167.0/22` and `91.108.4.0/22`). Having more than one IP increases resilience and makes future rotation less urgent.

```suggestion
const TELEGRAM_FALLBACK_IPS: readonly string[] = [
  "149.154.167.220", // primary fallback – verify periodically with `dig api.telegram.org`
];
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +144 to +149
(callback as (err: null, addresses: dns.LookupAddress[]) => void)(
null,
fallbackIps.map((addr) => ({ address: addr, family: 4 })),
);
} else {
(callback as (err: null, address: string, family: number) => void)(null, ip, 4);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 family is hardcoded to 4 for all fallback addresses

All entries in fallbackIps are reported as family: 4 regardless of their actual format. This is correct for the current single IPv4 entry, but if an IPv6 address is ever added to TELEGRAM_FALLBACK_IPS the lookup would advertise an IPv6 address as family 4, which undici would likely reject or misroute.

Consider detecting the family dynamically using net.isIPv4 / net.isIPv6:

Suggested change
(callback as (err: null, addresses: dns.LookupAddress[]) => void)(
null,
fallbackIps.map((addr) => ({ address: addr, family: 4 })),
);
} else {
(callback as (err: null, address: string, family: number) => void)(null, ip, 4);
if (opts && "all" in opts && opts.all) {
(callback as (err: null, addresses: dns.LookupAddress[]) => void)(
null,
fallbackIps.map((addr) => ({
address: addr,
family: net.isIPv4(addr) ? 4 : 6,
})),
);
} else {
(callback as (err: null, address: string, family: number) => void)(
null,
ip,
net.isIPv4(ip) ? 4 : 6,
);
}

(Requires import * as net from "node:net" at the top of the file.)

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/telegram/src/fetch.ts
Line: 144-149

Comment:
**`family` is hardcoded to `4` for all fallback addresses**

All entries in `fallbackIps` are reported as `family: 4` regardless of their actual format. This is correct for the current single IPv4 entry, but if an IPv6 address is ever added to `TELEGRAM_FALLBACK_IPS` the lookup would advertise an IPv6 address as family 4, which undici would likely reject or misroute.

Consider detecting the family dynamically using `net.isIPv4` / `net.isIPv6`:

```suggestion
    if (opts && "all" in opts && opts.all) {
      (callback as (err: null, addresses: dns.LookupAddress[]) => void)(
        null,
        fallbackIps.map((addr) => ({
          address: addr,
          family: net.isIPv4(addr) ? 4 : 6,
        })),
      );
    } else {
      (callback as (err: null, address: string, family: number) => void)(
        null,
        ip,
        net.isIPv4(ip) ? 4 : 6,
      );
    }
```

(Requires `import * as net from "node:net"` at the top of the file.)

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +527 to +529
fallbackIpDispatcher = stickyShouldUseEnvProxy
? new EnvHttpProxyAgent({ connect, proxyTls: connect })
: new Agent({ connect });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 connect and proxyTls share the same object reference

The established pattern throughout this file (see resolveTelegramDispatcherPolicy and createTelegramDispatcher) always passes independent copies of the connect options to connect and proxyTls:

connect: { ...connect }, proxyTls: { ...connect }

Here, both fields receive the same object reference. If EnvHttpProxyAgent ever mutates the options object during construction or request dispatch (e.g., to normalise or cache a field), mutations through one reference would silently affect the other. Align with the existing pattern:

Suggested change
fallbackIpDispatcher = stickyShouldUseEnvProxy
? new EnvHttpProxyAgent({ connect, proxyTls: connect })
: new Agent({ connect });
fallbackIpDispatcher = stickyShouldUseEnvProxy
? new EnvHttpProxyAgent({ connect: { ...connect }, proxyTls: { ...connect } })
: new Agent({ connect });
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/telegram/src/fetch.ts
Line: 527-529

Comment:
**`connect` and `proxyTls` share the same object reference**

The established pattern throughout this file (see `resolveTelegramDispatcherPolicy` and `createTelegramDispatcher`) always passes independent copies of the connect options to `connect` and `proxyTls`:

```ts
connect: { ...connect }, proxyTls: { ...connect }
```

Here, both fields receive the same object reference. If `EnvHttpProxyAgent` ever mutates the options object during construction or request dispatch (e.g., to normalise or cache a field), mutations through one reference would silently affect the other. Align with the existing pattern:

```suggestion
      fallbackIpDispatcher = stickyShouldUseEnvProxy
        ? new EnvHttpProxyAgent({ connect: { ...connect }, proxyTls: { ...connect } })
        : new Agent({ connect });
```

How can I resolve this? If you propose a fix, please make it concise.

@Cypherm Cypherm force-pushed the fix/telegram-ip-fallback branch from 29a3d59 to c040a6f Compare March 17, 2026 08:06
@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Mar 17, 2026

Superseded by #49148.

I kept the same fallback goal, but rewrote it as one ordered transport retry chain shared by normal Telegram API calls and media downloads, instead of adding another special-case branch inside resolveTelegramTransport().

@obviyus obviyus closed this Mar 17, 2026
@obviyus
Copy link
Copy Markdown
Contributor

obviyus commented Mar 29, 2026

Closing as duplicate; this was superseded by #49148.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: telegram Channel integration: telegram size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants