Skip to content

fix(telegram): add socket timeout and extract tgApi into shared module#1399

Open
tommylin-signalpro wants to merge 5 commits intoNVIDIA:mainfrom
tommylin-signalpro:refactor/telegram-api-module
Open

fix(telegram): add socket timeout and extract tgApi into shared module#1399
tommylin-signalpro wants to merge 5 commits intoNVIDIA:mainfrom
tommylin-signalpro:refactor/telegram-api-module

Conversation

@tommylin-signalpro
Copy link
Copy Markdown
Contributor

@tommylin-signalpro tommylin-signalpro commented Apr 3, 2026

Problem

tgApi() in scripts/telegram-bridge.js has no client-side socket timeout. When the TCP connection silently dies (e.g. NAT timeout, ISP routing change, network blip during the 30-second long-poll window), the HTTPS request hangs forever and the poll loop never recovers — the bot stops responding until the process is restarted.

Root cause: https.request() is called without a timeout option, and there is no timeout event handler. A dead TCP connection (no RST/FIN received) causes the request to wait indefinitely.

Solution

  1. Bug fix — add a 60s socket timeout with req.destroy() on the timeout event. Longer than Telegram's 30s server-side long-poll, so normal responses are unaffected. The existing poll() catch block already continues to the next iteration, so the loop self-heals.

  2. Extract shared module — move tgApi from an inline closure in telegram-bridge.js into bin/lib/telegram-api.js. Token is now a parameter instead of captured from module scope. This lets tests import the real production function instead of duplicating it.

Changes

File What
bin/lib/telegram-api.js New shared module — tgApi(token, method, body, opts) with timeout + opts for test overrides
scripts/telegram-bridge.js Replace 22-line inline tgApi with one-line wrapper calling shared module
test/telegram-api.test.js 6 vitest cases importing the real tgApi, verified against local TLS servers

Test plan

  • 6 test cases (no network dependency):
    • Normal response works with timeout in place
    • Server hang → timeout fires and rejects
    • Timeout fires within expected window (precision check)
    • Poll loop recovers after timeout (first call hangs, second succeeds)
    • Server closes connection mid-response (known limitation, documented)
    • Connection refused rejects immediately
  • CI: vitest + existing telegram bridge tests pass

Signed-off-by: Tommy Lin tommylin@signalpro.com.tw

Summary by CodeRabbit

  • Refactor

    • Telegram API communication extracted into a reusable shared client, improving reliability, configurable timeouts, and TLS/connection handling for messaging features.
  • Tests

    • Added end-to-end tests for the Telegram client covering successful responses, timeouts, connection failures, mid-response errors, and recovery across sequential calls.

Move the Telegram API client from an inline function in
telegram-bridge.js into bin/lib/telegram-api.js. The new module
accepts the bot token as a parameter instead of reading it from
module-level state, and includes a 60s socket timeout with
req.destroy() to prevent the poll loop from hanging on dead TCP
connections.

telegram-bridge.js now imports and wraps the shared function.
Tests in test/telegram-api.test.js import the same production
module and verify timeout, recovery, and error handling against
local TLS servers.

Refs: NVIDIA#1398

Signed-off-by: Tommy Lin <tommylin@signalpro.com.tw>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

Adds a shared Telegram Bot API client module (bin/lib/telegram-api.js), updates scripts/telegram-bridge.js to use it, and adds tests covering successful responses, timeouts, mid-response errors, and connection-refused scenarios.

Changes

Cohort / File(s) Summary
Telegram API Client
bin/lib/telegram-api.js
New CommonJS module exporting DEFAULT_TIMEOUT_MS (60000) and tgApi(token, method, body, opts = {}). Performs HTTPS POST to /bot{token}/{method}, sends JSON body, supports opts.timeout, opts.hostname, opts.port, opts.rejectUnauthorized, enforces timeouts by destroying sockets, guards against multiple settles, and returns parsed JSON or { ok: false, error: <raw string> } on parse failure.
Bridge Integration
scripts/telegram-bridge.js
Replaced inline HTTPS request logic with a delegated call to the imported tgApiRaw from ../bin/lib/telegram-api; existing control flow and result expectations ( { ok, result } shape) remain unchanged.
Test Suite
test/telegram-api.test.js
New Vitest tests that start local HTTPS servers with temporary self-signed certs, calling tgApi with per-call hostname/port/timeout to validate successful JSON responses, request timeouts (with elapsed-time checks), sequential recovery after a hang, mid-response socket destruction behavior, and connection-refused handling; servers are tracked and closed after tests.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Bridge as Bridge (scripts/telegram-bridge.js)
participant API as tgApi (bin/lib/telegram-api.js)
participant Telegram as Telegram API (api.telegram.org / test server)

Bridge->>API: tgApiRaw(TOKEN, method, body, opts)
API->>Telegram: HTTPS POST /bot{token}/{method} (JSON)
Telegram-->>API: HTTP response (chunks / end / error)
API-->>Bridge: Promise resolves/rejects with parsed JSON or error object

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇
I thumped my paws on sockets and streams,
Packaged JSON dreams into tidy beams.
Timeouts kept tidy, responses well-read,
Tests guard the burrow where messages thread.
Hooray — the shared tunnel hops ahead!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: extracting the inline tgApi function into a shared module and adding socket timeout functionality.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@tommylin-signalpro tommylin-signalpro changed the title refactor(telegram): extract tgApi into shared module fix(telegram): add socket timeout and extract tgApi into shared module Apr 3, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/lib/telegram-api.js`:
- Around line 47-57: The response handler for the HTTPS request only listens for
"data" and "end" which lets mid-stream errors or aborts hang the promise; update
the callback that receives the response (the res in the https.request callback)
to also attach res.on("error", ...) and res.on("aborted", ...) handlers that
immediately reject or resolve with an error object (consistent with the existing
resolve({ ok: false, error: ... })) so the outer Promise doesn't hang; apply the
same change to the other response handler instance referenced near line 61 to
ensure both places handle res errors/aborts.

In `@test/telegram-api.test.js`:
- Around line 156-160: The test "handles connection refused (server down)" races
because tempServer.close() is not awaited; change the close call to be awaited
by wrapping it in a Promise (e.g., await new Promise((resolve, reject) =>
tempServer.close(err => err ? reject(err) : resolve()))) so the server listener
is fully closed before issuing the API request; update the tempServer.close()
call in that test to use this awaited pattern referencing tempServer and the
test name to locate the change.
- Around line 69-75: The afterEach teardown is not awaiting server shutdowns so
handles linger; make afterEach async and await each server's close completion by
wrapping s.close in a Promise (or using a provided closeAsync) and resolving in
the close callback, ensuring s.closeAllConnections() is called first where
present; then await Promise.all over servers.map(...) (or sequential awaits) to
guarantee all s.close() completions before the test exits, and only then clear
the servers array.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 68bc192b-2545-4825-b290-7f896305a859

📥 Commits

Reviewing files that changed from the base of the PR and between aae3ccd and 241c738.

📒 Files selected for processing (3)
  • bin/lib/telegram-api.js
  • scripts/telegram-bridge.js
  • test/telegram-api.test.js

Address CodeRabbit review feedback on NVIDIA#1399:

- Add res.on("error") and res.on("aborted") handlers with a settle
  guard to prevent promise hangs on mid-stream socket errors.
- Make afterEach async and await server.close() to prevent lingering
  handles and port conflicts between tests.
- Await tempServer.close() in the connection-refused test to eliminate
  a race condition.
- Update mid-response test to assert rejection instead of accepting
  any outcome, now that aborted/error handlers are in place.

Signed-off-by: Tommy Lin <tommylin@signalpro.com.tw>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
bin/lib/telegram-api.js (1)

55-56: Make response decoding UTF-8 safe before JSON parse.

Line 56 appends raw chunks directly to a string. If multibyte UTF-8 characters are split across chunks, content can be corrupted. Set encoding on res (or accumulate Buffers and Buffer.concat) before parsing.

Proposed patch
     const req = https.request(reqOpts, (res) => {
       let buf = "";
+      res.setEncoding("utf8");
       res.on("data", (c) => (buf += c));
       res.on("aborted", () => settle(reject, new Error(`Telegram API ${method} response aborted`)));
       res.on("error", (err) => settle(reject, err));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/telegram-api.js` around lines 55 - 56, The response handling
currently appends raw chunks to a string via res.on("data", ...) which can
corrupt multibyte UTF-8 characters; update the handler in telegram-api.js around
the res callbacks to decode safely by either calling res.setEncoding('utf8')
before attaching data listeners or by accumulating Buffer objects (push each
chunk in res.on("data", chunk) into an array, then Buffer.concat on 'end' and
toString('utf8')) before JSON.parse; ensure the variable currently named buf and
the res.on("end", ...) JSON.parse logic use the properly decoded string or
buffer-to-string result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@bin/lib/telegram-api.js`:
- Around line 55-56: The response handling currently appends raw chunks to a
string via res.on("data", ...) which can corrupt multibyte UTF-8 characters;
update the handler in telegram-api.js around the res callbacks to decode safely
by either calling res.setEncoding('utf8') before attaching data listeners or by
accumulating Buffer objects (push each chunk in res.on("data", chunk) into an
array, then Buffer.concat on 'end' and toString('utf8')) before JSON.parse;
ensure the variable currently named buf and the res.on("end", ...) JSON.parse
logic use the properly decoded string or buffer-to-string result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b85a7a1b-9e58-4577-ac97-7aae7be784f4

📥 Commits

Reviewing files that changed from the base of the PR and between 241c738 and 5bc35da.

📒 Files selected for processing (2)
  • bin/lib/telegram-api.js
  • test/telegram-api.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/telegram-api.test.js

Add res.setEncoding("utf8") before attaching the data listener to
prevent multibyte UTF-8 characters from being corrupted when split
across chunks.

Signed-off-by: Tommy Lin <tommylin@signalpro.com.tw>
@wscurran wscurran added bug Something isn't working Integration: Telegram Use this label to identify Telegram bot integration issues with NemoClaw. labels Apr 3, 2026
@wscurran
Copy link
Copy Markdown
Contributor

wscurran commented Apr 3, 2026

✨ Thanks for submitting this pull request, which proposes a way to fix a reliability issue in the Telegram bridge by adding socket timeouts.

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

Labels

bug Something isn't working Integration: Telegram Use this label to identify Telegram bot integration issues with NemoClaw.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants