-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
[🐛 Bug]: Ruby: WebSocket hangs when receiving payload larger than WebSocket.max_frame_size #17264
Description
Description
When using Selenium with Ruby, the WebSocket connection used for DevTools (CDP) can hang if it receives a payload larger than WebSocket.max_frame_size (default: 20MB).
This issue is difficult to detect because, by default, no error is raised and the process appears to hang silently.
Root Cause
The issue is caused by the behavior of websocket-ruby and how it is used in Selenium.
- When the payload size exceeds
WebSocket.max_frame_size,websocket-rubyraises aTooLongexception during frame decoding: - However, unless
WebSocket.should_raise = trueis set, this exception is rescued internally andnilis returned: - In Selenium,
incoming_frame.nextis used to read messages:while (frame = incoming_frame.next)
- Once a frame exceeds
WebSocket.max_frame_size, decoding will always fail, but the loop continues callingnext, which keeps returningnil.
As a result, the WebSocket reading loop effectively becomes stuck, and the process appears to hang.
Reproduction
This issue occurs when a large payload is sent over the DevTools WebSocket connection.
The easiest way to reproduce is to reduce the frame size limit:
WebSocket.max_frame_size = 1Then trigger any CDP message.
In my case, the issue occurred under the following conditions:
- Use DevTools request interception (e.g.,
driver.intercept)
- This internally enables
Network.enable:
- Display a large preview image in the browser using a
data:image/...URL - CDP emits events such as:
Network.responseReceivedNetwork.requestWillBeSent- These include the large data: URL in the payload.
- The Ruby process hangs
Expected Behavior
When a frame exceeds WebSocket.max_frame_size, one of the following should happen:
- Raise an error by default, or at least provide a clear signal
- Log a warning or error message
- Avoid silently continuing with a broken decoding state
Actual Behavior
- No exception is raised by default
incoming_frame.nextcontinuously returns nil- The WebSocket read loop continues indefinitely
- The process appears to hang with no clear indication of the problem
Workarounds
- Increase the frame size limit:
WebSocket.max_frame_size = 100 * 1024 * 1024 - Enable exception raising:
WebSocket.should_raise = true - Avoid large CDP payloads
- For example, disable
Networkevents if they are not needed:driver.browser.devtools.network.disable- NOTE: this may break functionality that depends on network events, such as request cancellation tracking.
Suggestion
The main problem is that users cannot easily detect what is happening.
It would be helpful if Selenium:
- Raised an error when decoding fails repeatedly, or
- Logged a warning when frames are dropped or cannot be decoded
Additionally, it would be useful if CDP event subscriptions could be more granular, so that unnecessary Network.* events with large payloads do not need to be received when they are not needed.
Additional Notes
I apologize if this issue has already been reported.
If there is a better solution or a recommended approach, I would appreciate any guidance.
Reproducible Code
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "capybara"
gem "selenium-devtools"
gem "selenium-webdriver"
gem "puma"
gem "rackup"
end
require 'capybara'
require 'capybara/dsl'
require 'rack'
require 'rack/static'
require 'websocket'
html = <<~HTML
<!doctype html>
<html lang="en">
<body>
<form>
<input type="file" id="file" name="file" />
<img id="preview" src="" alt="Image preview" style="display: none; max-width: 200px; margin-top: 10px;" />
</form>
</body>
<script>
const form = document.querySelector('form');
const file = document.querySelector('#file');
const preview = document.querySelector('#preview');
file.addEventListener('change', () => {
const selectedFile = file.files[0];
if (selectedFile) {
const reader = new FileReader();
reader.onload = (e) => {
preview.src = e.target.result;
preview.style.display = 'block';
};
reader.readAsDataURL(selectedFile);
} else {
preview.src = '';
preview.style.display = 'none';
}
});
</script>
</html>
HTML
Capybara.default_driver = :selenium_chrome_headless
Capybara.app = Rack::ContentLength.new(
proc { [200, { "content-type" => "text/html" }, [html]] }
)
class Test
include Capybara::DSL
def run
# The following code causes the issue, but if you comment it out, it works fine.
page.driver.browser.intercept do |request, &continue|
continue.call(request)
end
WebSocket.max_frame_size = 1024
visit '/'
image = Tempfile.new(["avatar", ".png"]) do |f|
f.binmode
f.write OpenURI.open_uri("https://github.com/alpaca-tc.png").read
f.flush
end
attach_file("file", image.path)
end
end
Test.new.runℹ️ Last known working version: selenium-webdriver-4.41.0