Skip to content

fix(push): report noop and fetchMyEvents failures instead of leaving them unhandled - #243

Merged
EdamAme-x merged 1 commit into
evex-dev:mainfrom
sakamossan:fix/push-unhandled-rpc-rejections
Oct 10, 2026
Merged

EdamAme-x merged 1 commit into
evex-dev:mainfrom
sakamossan:fix/push-unhandled-rpc-rejections

Conversation

@sakamossan

Copy link
Copy Markdown
Contributor

Summary

Two RPC calls in ConnManager can reject with nobody watching, and when they do the host process exits with an unhandled rejection instead of keeping the push connection alive.

  1. _OnPingCallback fires this.client.talk.noop().then(...) on every third ping (every 90 s with the default 30 s ping interval). It has no .catch(), and Conn.onPacketReceived calls the callback synchronously and does not use its return value.
  2. _OnPushResponse awaits this.client.square.fetchMyEvents(...) for every service 3 (Square) push frame, but Conn calls the handler without awaiting it, so the returned promise is never observed either.

Both requests carry AbortSignal.timeout(config.timeout). When LINE is slow to answer, they reject with

TimeoutError: The operation was aborted due to timeout

(a DOMException, code: 23, whose stack has no frames from the caller because it comes from a timer). Since nothing catches it, Node and Deno both treat it as an unhandled rejection and terminate the process. We hit this in a long-running listener on Node: it ran fine for days and then exited on a single slow noop response.

Fix

  • _OnPingCallback: add .catch() after the .then(...) and report the error as this.log("noop failed", { error }) (listeners receive [LEGY/PUSH] noop failed). The connection is left as is; the next ping sends another noop.
  • _OnPushResponse: wrap the body of the service 3 branch in try/catch and report as this.log("fetchMyEvents failed", { error }). poll.sync.square and the subscription are only updated after a successful fetch, so the next push frame retries from the same sync token. A throw from readThriftStruct lands in the same catch.

As in #232 and #239, the log call in each catch is wrapped in its own try/catch, because log fans out to user-supplied listeners and a throwing listener would otherwise reject the handler and bring the unhandled rejection back.

I used new log types rather than LegyPusherError on purpose: a failed noop or fetchMyEvents is not a broken push connection, and consumers that treat LegyPusherError as "the pusher is down" should not react to it.

Testing

New packages/linejs/base/push/connManager.test.ts, 5 cases against a stub client passed as new ConnManager(client as never) (the same style as conn.test.ts), with talk.noop / square.fetchMyEvents rejecting with the same TimeoutError DOMException:

  1. _OnPingCallback(3) with a rejecting noop → one [LEGY/PUSH] noop failed carrying the original error, no unhandled rejection.
  2. Same, with a log listener that throws on the report → still no unhandled rejection.
  3. _OnPushResponse (not awaited) with a rejecting fetchMyEvents → [LEGY/PUSH] fetchMyEvents failed carrying the original error; poll.sync.square unchanged and no update:syncdata emitted.
  4. Same, with a log listener that throws on the report → still no unhandled rejection.
  5. A successful fetchMyEvents still enqueues the events on sqStream, updates poll.sync.square and the subscription id, emits update:syncdata and logs SQ_fetchMyEvents.

On main @ ef6c3d9, cases 1–4 fail with error: (in promise) TimeoutError: The operation was aborted due to timeout (Deno's sanitizer reports the unhandled rejection), and case 5 passes.

  • deno test -A at the repo root — 356 passed, 0 failed (351 on main @ ef6c3d9)
  • cd packages/linejs && deno publish --dry-run — success
  • deno check clean on both touched files. deno fmt --check is clean for the test file and for every line this PR changes in connManager.ts; it still reports two pre-existing lines in that file (176 and 300 on main), which I left alone to keep the diff focused.

This change touches only base/push/connManager.ts and adds its test.

🤖 Generated with Claude Code

…them unhandled

`_OnPingCallback` fires `talk.noop()` every third ping (every 90 s) without
a catch, and Conn calls `_OnPushResponse` without awaiting it, so a request
that hits `config.timeout` rejects with an `AbortSignal.timeout`
TimeoutError that nobody observes. Node and Deno treat that as an
unhandled rejection and terminate the host process.

Catch both and report them through `ConnManager.log` as `noop failed` /
`fetchMyEvents failed`, keeping the connection alive. As in evex-dev#232 and evex-dev#239,
the report is wrapped in try/catch because `log` fans out to user-supplied
listeners and a throwing listener would otherwise bring the unhandled
rejection back.
@EdamAme-x
EdamAme-x merged commit 4fc512a into evex-dev:main Oct 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants