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
- Start a QQ bridge and arrange for its cached token to need refreshing (e.g. return expires_in: 1 from the initial token request).
- Deliver HELLO (op 10). For Resume, first deliver READY with session_id and sequence. Hold the resulting getAppAccessToken response pending.
- Close/reconnect the old socket, or stop and start the same bridge; complete the new gateway handshake and deliver READY on the replacement socket.
- Complete the old token request with either a failure or a successful token.
- 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
- Start Discord/QQ/DingTalk, or let an automatic reconnect enter its gateway HTTP request. Hold /gateway/bot or /v1.0/gateway/connections/open pending.
- Await bridge.stop(). Optionally start again and finish the newer gateway request first.
- Release the older request with a valid gateway response.
- 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.
What happened
Async bot handshakes can still mutate a bridge after their owning connection has been stopped or replaced:
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
B. Pending gateway handshake finishes after stop
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:
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.