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-ruby raises a TooLong exception during frame decoding:
- However, unless
WebSocket.should_raise = true is set, this exception is rescued internally and nil is returned:
- In Selenium,
incoming_frame.next is 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 calling next, which keeps returning nil.
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 = 1
Then trigger any CDP message.
In my case, the issue occurred under the following conditions:
- Use DevTools request interception (e.g.,
driver.intercept)
- Display a large preview image in the browser using a
data:image/... URL
- CDP emits events such as:
Network.responseReceived
Network.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.next continuously 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
Network events 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
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-rubyand how it is used in Selenium.WebSocket.max_frame_size,websocket-rubyraises aTooLongexception during frame decoding:WebSocket.should_raise = trueis set, this exception is rescued internally andnilis returned:incoming_frame.nextis used to read messages:selenium/rb/lib/selenium/webdriver/common/websocket_connection.rb
Line 135 in fda2c56
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:
Then trigger any CDP message.
In my case, the issue occurred under the following conditions:
driver.intercept)Network.enable:data:image/...URLNetwork.responseReceivedNetwork.requestWillBeSentExpected Behavior
When a frame exceeds
WebSocket.max_frame_size, one of the following should happen:Actual Behavior
incoming_frame.nextcontinuously returns nilWorkarounds
WebSocket.max_frame_size = 100 * 1024 * 1024WebSocket.should_raise = trueNetworkevents if they are not needed:driver.browser.devtools.network.disableSuggestion
The main problem is that users cannot easily detect what is happening.
It would be helpful if Selenium:
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
ℹ️ Last known working version:
selenium-webdriver-4.41.0