fix(telegram): unify transport fallback chain#49148
Conversation
Greptile SummaryThis PR replaces the dual Key changes:
Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/media/fetch.ts
Line: 155-156
Comment:
**Intermediate attempt errors silently discarded in 3-attempt chains**
`primaryError ??= err` only records the first failure. With the new three-tier chain (default → IPv4-only → pinned IP), the second attempt's error (IPv4-only) is never stored anywhere: `primaryError` already holds attempt 0's error and is never updated after that.
When the chain exhausts, the combined error reports `primaryError = err[0]` and `cause = err[2]`, silently dropping `err[1]`. For debugging a multi-tier Telegram connectivity failure this can obscure the real sequence.
A simple fix is to accumulate all errors, for example as a chain:
```typescript
// instead of primaryError ??= err:
primaryError = primaryError === undefined ? err : new Error("retry attempt failed", { cause: err });
```
Or, if the combined-error message only ever needs to mark "there was a first error and a last error", the current shape is at least documented — but it should have a comment explaining the intentional loss of middle attempts.
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: 160-165
Comment:
**No guard for empty `addresses` in `createPinnedHostnameLookup`**
`selected[0]` can be `undefined` when `pinnedHostname.addresses` is an empty array, causing `first.address` to throw at runtime. Today this is prevented by the external guard in `createTelegramTransportAttempts`:
```typescript
if (TELEGRAM_FALLBACK_IPS.length === 0) {
return attempts;
}
```
…but `createPinnedHostnameLookup` accepts any `PinnedDispatcherPolicy["pinnedHostname"]` and has no enforcement on its own. A caller that passes an empty `addresses` list (e.g. from a `PinnedHostnameOverride` with `addresses: []`) would get an opaque crash on the first DNS resolution.
Consider adding a guard before the `selected[0]` dereference:
```suggestion
const first = selected[0];
if (!first) {
resolvedCallback(new Error(`No usable address for pinned hostname ${pinnedHostname.hostname}`) as unknown as null, "", 0);
return;
}
(resolvedCallback as (err: null, address: string, family: number) => void)(
null,
first.address,
first.family,
);
```
Or at minimum a non-null assertion with an explanatory comment pointing at the invariant enforced by the caller.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/media/fetch.ts
Line: 158-160
Comment:
**Unreachable defensive guard**
After the loop, `result` is either set (successful attempt + `break`) or an error was thrown. Since `attempts` is guaranteed non-empty (the fallback `[{ dispatcherPolicy: undefined, lookupFn }]` ensures at least one element), the `if (!result)` branch can never execute. This makes `result` typed as `... | undefined` when in practice it's always defined at this point, which forces the `!result` check to satisfy TypeScript rather than guard against a real case.
Replacing `let result: ... | undefined` with a typed `let result!: ...` (definite-assignment assertion) or restructuring to assign at the point of success would make the intent clearer and remove the dead branch.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 2d71d48 |
2d71d48 to
efbeef1
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2d71d4861c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
8389e56 to
ea0a1a7
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ea0a1a7853
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!normalizedOverrideHost || normalizedOverrideHost !== pinned.hostname) { | ||
| throw new Error( | ||
| `Pinned dispatcher override hostname mismatch: expected ${pinned.hostname}, got ${override.hostname}`, | ||
| ); |
There was a problem hiding this comment.
Skip override mismatch errors on redirected hosts
The new hostname equality check throws whenever dispatcherPolicy.pinnedHostname.hostname differs from the currently resolved host, but fetchWithSsrFGuard reuses the same dispatcher policy across redirect hops. In the new Telegram fallback chain, if the pinned-IP attempt receives a redirect to another host (for example a CDN hostname), this branch throws before the redirected request is attempted, so the final fallback can fail even though the redirect target is otherwise allowed and reachable.
Useful? React with 👍 / 👎.
* main: (142 commits) fix(zalouser): fix setup-only onboarding flow (openclaw#49219) CI: add built plugin singleton smoke (openclaw#48710) update contributing focus areas docs(providers): clarify provider capabilities vs public capability model docs(refactor): align plugin SDK plan with public capability model docs(cli): add plugins inspect command reference docs(plugins): document public capability model, plugin shapes, and inspection Plugins: internalize diagnostics OTel imports Plugins: internalize diffs SDK imports Plugins: internalize more extension SDK imports Plugins: add local extension API barrels Plugins: add inspect matrix and trim export Plugins: add inspect command and capability report fix(telegram): unify transport fallback chain (openclaw#49148) Plugins: add binding resolution callbacks (openclaw#48678) fix(gateway): clear trusted-proxy control ui scopes refactor: narrow extension public seams test: stabilize memory async search close docs(hooks): clarify trust model and audit guidance feat(mattermost): add retry logic and timeout handling for DM channel creation (openclaw#42398) ...
* fix(telegram): unify transport fallback chain * fix: address telegram fallback review comments * fix: validate pinned SSRF overrides * fix: unify telegram fallback retries (openclaw#49148)
* fix(telegram): unify transport fallback chain * fix: address telegram fallback review comments * fix: validate pinned SSRF overrides * fix: unify telegram fallback retries (openclaw#49148)
* fix(telegram): unify transport fallback chain * fix: address telegram fallback review comments * fix: validate pinned SSRF overrides * fix: unify telegram fallback retries (openclaw#49148)
* fix(telegram): unify transport fallback chain * fix: address telegram fallback review comments * fix: validate pinned SSRF overrides * fix: unify telegram fallback retries (openclaw#49148)
* fix(telegram): unify transport fallback chain * fix: address telegram fallback review comments * fix: validate pinned SSRF overrides * fix: unify telegram fallback retries (openclaw#49148)
Summary
Supersedes #48812 with a simpler cut.
Before:
resolveTelegramTransport()had one sticky IPv4 fallback path for normal API calls.pinnedDispatcherPolicy+fallbackPinnedDispatcherPolicypair.Insight:
After:
PinnedDispatcherPolicyso the Telegram fallback IP is represented in the policy, not as bespoke runtime state.Testing
pnpm test -- extensions/telegram/src/fetch.test.ts src/media/fetch.telegram-network.test.ts src/infra/net/ssrf.dispatcher.test.ts extensions/telegram/src/bot/delivery.resolve-media-retry.test.ts src/media/fetch.test.tspnpm tsgo