Skip to content

fix: guard all ws.send() calls with readyState check to prevent server crashes#3936

Merged
evanpelle merged 1 commit into
openfrontio:mainfrom
berkelmali:fix/guard-ws-send-readystate
May 16, 2026
Merged

fix: guard all ws.send() calls with readyState check to prevent server crashes#3936
evanpelle merged 1 commit into
openfrontio:mainfrom
berkelmali:fix/guard-ws-send-readystate

Conversation

@berkelmali
Copy link
Copy Markdown
Contributor

@berkelmali berkelmali commented May 16, 2026

Description:

Several ws.send() calls in GameServer.ts were missing WebSocket.OPEN readyState guards. This can lead to server crashes if a client disconnects precisely between a check and the send. Added guards to prestart, kickClient, and handleSynchronization.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

barfires

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

Review Change Stack

Walkthrough

GameServer now checks a client's WebSocket readyState === WebSocket.OPEN before sending messages or closing the socket in prestart(), kickClient(), and handleSynchronization() to avoid writes on non-open sockets.

Changes

WebSocket Readystate Guards

Layer / File(s) Summary
readyState checks for prestart, kick, and desync sends
src/server/GameServer.ts
prestart() now checks each client's socket is OPEN before sending; kickClient() sends the kick/error payload and closes the socket only when the socket is OPEN; handleSynchronization() sends desync messages only to OPEN sockets.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A small guard stands at every door,
Checking state before sending more,
No frantic writes to sockets gone,
Only open doors receive the song,
Safe messages travel on.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding WebSocket readyState checks before send() calls to prevent crashes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description clearly explains the problem (missing WebSocket.OPEN guards), identifies affected code locations, and describes the fix applied.

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server/GameServer.ts`:
- Around line 1004-1012: The review calls out remaining unguarded WebSocket
sends in joinClient and sendStartGameMsg (and the shown error-send/close block)
that can throw if the socket disconnects; update each place (joinClient,
sendStartGameMsg, and the error-send in GameServer where you check
client.ws.readyState) to 1) verify client.ws.readyState === WebSocket.OPEN
before calling ws.send, and 2) wrap ws.send in a try/catch to swallow/log send
errors so a transient disconnect doesn’t crash the flow, keeping the subsequent
ws.close call safe and preserving existing error logging semantics.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa49ede5-50d4-4590-b2f7-70ae23acece7

📥 Commits

Reviewing files that changed from the base of the PR and between b813792 and 201059f.

📒 Files selected for processing (1)
  • src/server/GameServer.ts

Comment thread src/server/GameServer.ts
Comment on lines +1004 to +1012
if (client.ws.readyState === WebSocket.OPEN) {
client.ws.send(
JSON.stringify({
type: "error",
error: reasonKey,
} satisfies ServerErrorMessage),
);
client.ws.close(1000, reasonKey);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

PR objective is not fully met: two ws.send() calls are still unguarded

Nice fix here, but there are still unguarded sends at Line 211 (joinClient) and Line 786 (sendStartGameMsg). Those paths can still throw if the socket disconnects at the wrong time.

Suggested patch
@@
-      client.ws.send(
-        JSON.stringify({
-          type: "error",
-          error: "full-lobby",
-        } satisfies ServerErrorMessage),
-      );
+      if (client.ws.readyState === WebSocket.OPEN) {
+        client.ws.send(
+          JSON.stringify({
+            type: "error",
+            error: "full-lobby",
+          } satisfies ServerErrorMessage),
+        );
+      }
@@
-      ws.send(
-        JSON.stringify({
-          type: "start",
-          turns: this.turns.slice(lastTurn),
-          gameStartInfo: this.gameStartInfo,
-          lobbyCreatedAt: this.createdAt,
-          myClientID: client.clientID,
-        } satisfies ServerStartGameMessage),
-      );
+      if (ws.readyState === WebSocket.OPEN) {
+        ws.send(
+          JSON.stringify({
+            type: "start",
+            turns: this.turns.slice(lastTurn),
+            gameStartInfo: this.gameStartInfo,
+            lobbyCreatedAt: this.createdAt,
+            myClientID: client.clientID,
+          } satisfies ServerStartGameMessage),
+        );
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/GameServer.ts` around lines 1004 - 1012, The review calls out
remaining unguarded WebSocket sends in joinClient and sendStartGameMsg (and the
shown error-send/close block) that can throw if the socket disconnects; update
each place (joinClient, sendStartGameMsg, and the error-send in GameServer where
you check client.ws.readyState) to 1) verify client.ws.readyState ===
WebSocket.OPEN before calling ws.send, and 2) wrap ws.send in a try/catch to
swallow/log send errors so a transient disconnect doesn’t crash the flow,
keeping the subsequent ws.close call safe and preserving existing error logging
semantics.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management May 16, 2026
…r crashes

Several ws.send() calls in GameServer were missing WebSocket.OPEN
readyState guards. If a client disconnects between being iterated and
the send() call, the unguarded send throws an exception that can crash
the entire game server process, affecting all connected players.

The broadcastLobbyInfo() method already had this guard correctly,
confirming this was an oversight in the other methods.

Affected methods:
- endTurn(): called every ~100ms, highest crash risk
- prestart(): called when transitioning lobby to game
- kickClient(): crash during kick operation
- handleSynchronization(): called every 10 turns for desync detection
@berkelmali berkelmali force-pushed the fix/guard-ws-send-readystate branch from 201059f to 6619c5a Compare May 16, 2026 14:16
@evanpelle evanpelle added this to the v32 milestone May 16, 2026
@evanpelle evanpelle merged commit 48b957c into openfrontio:main May 16, 2026
11 of 16 checks passed
@github-project-automation github-project-automation Bot moved this from Development to Complete in OpenFront Release Management May 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

4 participants