[rb] fix silent hang on oversized WebSocket frames#17710
[rb] fix silent hang on oversized WebSocket frames#17710Chandan25sharma wants to merge 4 commits into
Conversation
WebSocket.max_frame_size (websocket-ruby default 20 MB) is bumped to 100 MB in WebSocketConnection#initialize so typical DevTools payloads pass through. For frames that still exceed the limit, websocket-ruby rescues TooLong internally and returns nil from incoming_frame.next, leaving oversized bytes buffered and causing the read loop to spin at ~100% CPU. After the inner while loop we now check incoming_frame.error? and raise the stored error so the outer loop exits cleanly and the connection is logged and closed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR Summary by QodoFix Ruby WebSocket hang on oversized frames (raise TooLong + bump max size) Description
Diagram
High-Level Assessment
Files changed (3)
|
…> ambiguity IEventStream<T> implements both IAsyncEnumerable<T> and IAsyncDisposable. TaskAsyncEnumerableExtensions ships a ConfigureAwait overload for each interface, so a plain stream.ConfigureAwait(bool) call on an IEventStream<T> variable produces CS0121 (ambiguous call). Add EventStreamExtensions.ConfigureAwait<T>(IEventStream<T>, bool) which explicitly routes to the IAsyncEnumerable<T> overload (the behaviour callers need for await foreach). Because the new overload is more specific than the two BCL overloads, the compiler resolves to it without any cast on the call site. Fixes: SeleniumHQ#17696 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Problem
-------
IEventStream<T> inherits both IAsyncEnumerable<T> and IAsyncDisposable.
System.Linq.Async ships a ConfigureAwait overload for each of those
interfaces in TaskAsyncEnumerableExtensions:
ConfigureAwait<T>(IAsyncEnumerable<T>, bool) -> ConfiguredCancelableAsyncEnumerable<T>
ConfigureAwait(IAsyncDisposable, bool) -> ConfiguredAsyncDisposable
Both overloads match when the receiver is an IEventStream<T>, so the
compiler emits CS0121 ("The call is ambiguous between ..."). Users had
to add an explicit cast to either interface to work around this, which
is both surprising and hard to discover.
Fix
---
Add EventStreamExtensions.ConfigureAwait<T>(this IEventStream<T>, bool)
that explicitly delegates to the IAsyncEnumerable<T> overload. Because
a more-derived receiver type takes priority over the two BCL overloads,
the compiler now resolves the call unambiguously without any cast at the
call site. The IAsyncEnumerable<T> variant is the correct choice here
since callers use ConfigureAwait to control context capture inside
`await foreach` loops, not during disposal.
Testing
-------
EventStreamExtensionsTests verifies at the type level that the return
type is ConfiguredCancelableAsyncEnumerable<T> (not ConfiguredAsyncDisposable)
and at the behavioural level that events flow through a
ConfigureAwait(false) enumeration correctly.
Fixes: SeleniumHQ#17696
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| namespace OpenQA.Selenium.BiDi; | ||
|
|
||
| public static class EventStreamExtensions | ||
| { |
There was a problem hiding this comment.
1. Public class missing xml summary 📘 Rule violation ✧ Quality
EventStreamExtensions is a new public class but it has no XML documentation comment with a <summary> immediately preceding the declaration. This violates the requirement that all public API members include <summary> docs for tooling and consumers.
Agent Prompt
## Issue description
A new public C# type (`EventStreamExtensions`) was added without an XML documentation comment containing a non-empty `<summary>`.
## Issue Context
Compliance requires XML docs with `<summary>` for all public API members.
## Fix Focus Areas
- dotnet/src/webdriver/BiDi/EventStreamExtensions.cs[22-25]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| # websocket-ruby rescues TooLong internally and returns nil from next; | ||
| # raise the stored error so the loop exits instead of spinning at 100% cpu | ||
| raise incoming_frame.error if incoming_frame.error? |
There was a problem hiding this comment.
3. Listener failure causes timeouts 🐞 Bug ☼ Reliability
When attach_socket_listener raises incoming_frame.error, the surrounding rescue only logs and exits the listener thread without closing the socket or surfacing the error to waiting callers. After the listener exits, send_cmd can still successfully write but then waits until its 30s timeout because responses are never enqueued.
Agent Prompt
## Issue description
The listener thread can terminate on `incoming_frame.error`, but the error is only logged; callers waiting in `send_cmd` are not notified and may block until `Wait` times out.
## Issue Context
- `send_cmd` writes to the socket and then waits for `messages.delete(id)`.
- `attach_socket_listener` rescues and logs but does not close the socket or store/propagate the failure.
## Fix Focus Areas
- rb/lib/selenium/webdriver/common/websocket_connection.rb[104-118]
- rb/lib/selenium/webdriver/common/websocket_connection.rb[131-158]
### Suggested fix
1. In `attach_socket_listener`, when an error occurs (either `incoming_frame.error?` path or in the rescue), store it on the connection (e.g., `@listener_error = e`) and transition the connection to a closed/closing state.
2. Close the underlying socket in that error path so subsequent `send_cmd` writes fail fast.
3. In `send_cmd`’s wait loop, check `@listener_error` (or `@closing`) and raise immediately instead of waiting for the timeout.
Example sketch:
- In listener rescue: set `@listener_error ||= e`; set `@closing = true`; `socket.close rescue nil`.
- In `send_cmd` wait block: `raise @listener_error if @listener_error` before/alongside checking `messages.delete(id)`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Added MAX_FRAME_SIZE = 100 * 1024 * 1024 (100 MB) constant
In initialize: bumps WebSocket.max_frame_size to MAX_FRAME_SIZE only when the current global is lower, so user-configured values are never overridden
In attach_socket_listener: after the inner while (frame = incoming_frame.next) loop exits, checks incoming_frame.error? and raises the stored error — this breaks the outer read loop instead of spinning at 100% CPU
websocket_connection.rbs
Added MAX_FRAME_SIZE: Integer to the type signature
websocket_connection_spec.rb (new)
Tests that MAX_FRAME_SIZE is 100 MB
Tests that initialize bumps the global frame-size limit when it's below MAX_FRAME_SIZE
Tests that initialize leaves a higher value untouched