-
Notifications
You must be signed in to change notification settings - Fork 42
fix: prevent unsubscribe errors by checking WebSocket state before sending #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR addresses WebSocket unsubscribe errors that occur when clients disconnect by preventing unsubscribe operations on non-open connections and filtering specific error messages.
- Silently return from
unsubscribe()when WebSocket is not OPEN instead of logging a warning - Add try-catch around
ws.send()and only log errors when connection is still OPEN - Filter "invalid request body" and "unsubscribe" error messages in
onMessageto reduce log noise
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| } | ||
| }); | ||
| } catch (error) { |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empty catch block silently swallows all errors. This could hide legitimate issues like JSON serialization errors or programming mistakes. Consider at least logging unexpected errors or being more specific about which errors to ignore.
| } catch (error) { | |
| } catch (error) { | |
| console.error("unsubscribe exception", error); |
| } catch { | ||
| // Not JSON, ignore silently | ||
| } | ||
| } |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling logic removes the original error log when the WebSocket is not OPEN (line 168 removed the unconditional console.log("onMessage error", { event })), but doesn't restore it for legitimate errors. This means valid error messages that don't match the filter criteria will be silently ignored. Consider adding back an error log for cases that don't match the suppression filters.
| } catch { | |
| // Not JSON, ignore silently | |
| } | |
| } | |
| // If we reach here, it's a parsed message that isn't suppressed; log it | |
| console.error("onMessage error (unhandled message):", event.data); | |
| } catch { | |
| // Not JSON, log the raw event data for visibility | |
| console.error("onMessage error (non-JSON message):", event.data); | |
| } |
src/client.ts
Outdated
| const parsed = JSON.parse(event.data); | ||
| if (parsed.message && typeof parsed.message === "string") { | ||
| const errorMsg = parsed.message.toLowerCase(); | ||
| if (errorMsg.includes("invalid request body") || errorMsg.includes("unsubscribe")) { |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking for "unsubscribe" in error messages is too broad and could suppress legitimate error messages that happen to contain this word. For example, an error like "Failed to unsubscribe from topic X" would be silently ignored. Consider being more specific about which error messages to filter, or only filter "invalid request body" errors.
| if (errorMsg.includes("invalid request body") || errorMsg.includes("unsubscribe")) { | |
| if (errorMsg.includes("invalid request body")) { |
aa76492 to
28f99ad
Compare
28f99ad to
6352798
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/client.ts
Outdated
| ws.send(JSON.stringify({ action: "subscribe", ...msg }), (err?: Error) => { | ||
| if (err) { | ||
| console.error("subscribe error", err); | ||
| this.ws.close(); | ||
| ws.close(); | ||
| } | ||
| }); |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The subscribe method should also wrap ws.send() in a try-catch block for consistency with the unsubscribe method. If ws.send() can throw synchronous errors during unsubscribe (which is why a try-catch was added), the same could happen during subscribe operations.
| }); | ||
| } catch (error) { | ||
| if (ws.readyState === WebSocket.OPEN) { | ||
| console.error("unsubscribe exception", error); |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a synchronous error is caught from ws.send() (line 227), the error is logged but the WebSocket is not closed. This is inconsistent with the callback error handler on line 223, which closes the connection after logging the error. Consider adding ws.close(); after line 229 to maintain consistent error handling behavior.
| console.error("unsubscribe exception", error); | |
| console.error("unsubscribe exception", error); | |
| ws.close(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const ws = this.ws; | ||
| try { | ||
| ws.send(JSON.stringify({ action: "subscribe", ...msg }), (err?: Error) => { | ||
| if (err) { | ||
| if (ws.readyState === WebSocket.OPEN) { | ||
| console.error("subscribe error", err); | ||
| ws.close(); | ||
| } | ||
| } | ||
| }); | ||
| } catch (error) { | ||
| if (ws.readyState === WebSocket.OPEN) { | ||
| console.error("subscribe exception", error); | ||
| ws.close(); | ||
| } | ||
| }); | ||
| } |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition: The local ws variable captures this.ws at the time of the method call, but this.ws can be reassigned during reconnection (in the connect() method). The asynchronous callback and catch block check ws.readyState, which refers to the old WebSocket instance, not the current one. This could lead to incorrect behavior if reconnection happens between the send() call and the callback execution.
Consider checking this.ws.readyState instead of ws.readyState, or ensure the callback/error handler operates on the correct WebSocket instance.
| const ws = this.ws; | ||
| try { | ||
| ws.send(JSON.stringify({ action: "unsubscribe", ...msg }), (err?: Error) => { | ||
| if (err) { | ||
| if (ws.readyState === WebSocket.OPEN) { | ||
| console.error("unsubscribe error", err); | ||
| ws.close(); | ||
| } | ||
| } | ||
| }); | ||
| } catch (error) { | ||
| if (ws.readyState === WebSocket.OPEN) { | ||
| console.error("unsubscribe exception", error); | ||
| ws.close(); | ||
| } | ||
| }); | ||
| } |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition: The local ws variable captures this.ws at the time of the method call, but this.ws can be reassigned during reconnection (in the connect() method). The asynchronous callback and catch block check ws.readyState, which refers to the old WebSocket instance, not the current one. This could lead to incorrect behavior if reconnection happens between the send() call and the callback execution.
Consider checking this.ws.readyState instead of ws.readyState, or ensure the callback/error handler operates on the correct WebSocket instance.
| const ws = this.ws; | ||
| try { | ||
| ws.send(JSON.stringify({ action: "subscribe", ...msg }), (err?: Error) => { | ||
| if (err) { | ||
| if (ws.readyState === WebSocket.OPEN) { | ||
| console.error("subscribe error", err); | ||
| ws.close(); | ||
| } | ||
| } | ||
| }); | ||
| } catch (error) { | ||
| if (ws.readyState === WebSocket.OPEN) { | ||
| console.error("subscribe exception", error); | ||
| ws.close(); | ||
| } | ||
| }); | ||
| } |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Inconsistent error handling: The ping() method at line 151 doesn't use the same try-catch pattern or state-aware error handling that was added to subscribe() and unsubscribe(). For consistency and to prevent similar errors during disconnection, consider applying the same error handling pattern to ping().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| if (errorMsg.includes("invalid request body")) { | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silently swallowing all "invalid request body" server errors
Medium Severity
The onMessage handler silently suppresses all server responses containing "invalid request body", not just ones caused by the disconnect race condition. If a genuinely malformed subscribe or unsubscribe request is sent (e.g., wrong fields), the server's error response is silently dropped with no logging, making it very difficult to debug subscription failures. The readyState guards already added to subscribe/unsubscribe prevent the race condition; this broad suppression in onMessage masks legitimate API errors.


Problem
When clients disconnect,
unsubscribe()attempts to send messages even when the WebSocket is already closing, causing"Invalid request body"errors that pollute logs.Solution
readyState !== WebSocket.OPENbefore attempting unsubscribe (silently return if not ready)ws.send()in try-catch to handle synchronous errorsconsole.log("unsubscribing", ...)debug logonMessageTesting
Tested unsubscribe during disconnection - no errors appear
Verified actual errors still logged when connection is open
No breaking changes
Related Issues
#21
Note
Low Risk
Small, localized changes to client-side WebSocket error handling and logging; main risk is masking an unexpected server error string match or altering when sockets are closed on send failures.
Overview
Reduces noisy WebSocket error logging during disconnect/teardown by ignoring server messages whose JSON
messagecontains "invalid request body".Hardens
subscribe()/unsubscribe()by returning early when the socket is notOPEN, wrappingws.send()intry/catchto handle synchronous send failures, and only logging/closing the socket on callback errors when the connection is stillOPEN(also removing theunsubscribingdebug log).Written by Cursor Bugbot for commit ac1f5fd. This will update automatically on new commits. Configure here.