Skip to content

bug(bots): stale async handshakes outlive stopped or replaced connections #5949

Description

@jackeyfaker77

What happened

Async bot handshakes can still mutate a bridge after their owning connection has been stopped or replaced:

  • GatewayBridgeBase.sendIdentify/sendResume await the payload builder, then use the mutable current socket. An old QQ token refresh returning null can close the replacement socket and schedule another reconnect; a successful old result can send its authentication payload through the replacement socket.
  • GatewayBridgeBase.openConnection and DingTalkBotBridge.openConnection can finish an old HTTP request after stop(), create an unowned socket, or overwrite a newer stop/start connection. Old HTTP failures can overwrite the stopped/current status; old token successes can overwrite the current token cache.

Expected: only the current connection attempt/socket may connect, update handshake status/token state, send authentication, or retry. Current requests should keep their normal authentication and reconnect behavior.

How to reproduce

These races reproduce deterministically with deferred HTTP responses and fake WebSockets; no live credentials are required.

A. Pending Identify/Resume acts on a replacement socket

  1. Start a QQ bridge and arrange for its cached token to need refreshing (e.g. return expires_in: 1 from the initial token request).
  2. Deliver HELLO (op 10). For Resume, first deliver READY with session_id and sequence. Hold the resulting getAppAccessToken response pending.
  3. Close/reconnect the old socket, or stop and start the same bridge; complete the new gateway handshake and deliver READY on the replacement socket.
  4. Complete the old token request with either a failure or a successful token.
  5. Observe that the failure can close the replacement socket and schedule a retry, while success can send old op 2/op 6 authentication through the replacement. HTTP failure also changes the current reason.

B. Pending gateway handshake finishes after stop

  1. Start Discord/QQ/DingTalk, or let an automatic reconnect enter its gateway HTTP request. Hold /gateway/bot or /v1.0/gateway/connections/open pending.
  2. Await bridge.stop(). Optionally start again and finish the newer gateway request first.
  3. Release the older request with a valid gateway response.
  4. Observe a WebSocket created after stop, or a second socket replacing the newer one. Release it with HTTP/network failure instead to observe stale status updates and, after restart, an unwanted reconnect.

Holding the initial QQ/DingTalk token request across stop/start reproduces stale token-cache/status writes too.

The follow-up regression fixture will use the production provider HTTP methods (Undici MockAgent), deferred responses and socket factory seam. The final 38-case fixture against unchanged main reports 35 failures and 3 passing current-request retry controls; against the fix all 38 pass.

Environment

Logs, screenshots, or additional context

Refs #5947 and #5948. Their retired WebSocket event-entry protection addresses late events; this issue tracks async work that entered while its connection was current and resumed after retirement. The problem is present on the base commit as well.

Observed before the fix:

  • retired auth failure: replacement socket closed; reconnect scheduled
  • retired auth success: replacement socket receives retired authentication
  • gateway success after stop: a new unclosed socket is created
  • stale HTTP/token failures: stopped/current readiness and reason are overwritten

A separate repair PR will link this issue and include the regression fixture. Existing in-flight HTTP requests can finish and their bodies be consumed; stale results must be discarded.

AI disclosure: this report and its reproduction/repair work were prepared with OpenAI Codex on behalf of the contributor.

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions