Skip to content

Commit 7b0a8a1

Browse files
authored
fix: JSON::NestingError for deeply nested JSON object (#498)
1 parent 5ca5e9e commit 7b0a8a1

File tree

10 files changed

+380
-59
lines changed

10 files changed

+380
-59
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- `:ws_url` option is now used without modifications WYSIWYG.
1919
- `Network.requestWillBeSent` callback didn't handle params in a type-safe way
2020
- `Page.frameStoppedLoading` callback shouldn't wait for document_node_id response
21+
- `JSON::NestingError` is raised when browser returns very deeply nested JSON and crashes the thread [#498]
2122

2223
### Removed
2324

lib/ferrum/client/web_socket.rb

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,16 @@ def on_open(_event)
5050
end
5151

5252
def on_message(event)
53-
data = begin
54-
JSON.parse(event.data)
55-
rescue JSON::ParserError
56-
unescaped = unescape_unicode(event.data)
57-
.encode("UTF-8", "UTF-8", undef: :replace, invalid: :replace, replace: "?")
58-
JSON.parse(unescaped)
59-
end
60-
61-
@messages.push(data)
53+
data = safely_parse_json(event.data)
54+
# If we couldn't parse JSON data for some reason (parse error or deeply nested object) we
55+
# don't push response to @messages. Worse that could happen we raise timeout error due to command didn't return
56+
# anything or skip the background notification, but at least we don't crash the thread that crashes the main
57+
# thread and the application.
58+
@messages.push(data) if data
6259

6360
output = event.data
64-
if SKIP_LOGGING_SCREENSHOTS && @screenshot_commands[data["id"]]
65-
@screenshot_commands.delete(data["id"])
61+
if SKIP_LOGGING_SCREENSHOTS && @screenshot_commands[data&.dig("id")]
62+
@screenshot_commands.delete(data&.dig("id"))
6663
output.sub!(/{"data":"[^"]*"}/, %("Set FERRUM_LOGGING_SCREENSHOTS=true to see screenshots in Base64"))
6764
end
6865

@@ -108,8 +105,21 @@ def start
108105
end
109106
end
110107

111-
def unescape_unicode(value)
112-
value.gsub(/\\u([\da-fA-F]{4})/) { |_| [::Regexp.last_match(1)].pack("H*").unpack("n*").pack("U*") }
108+
def safely_parse_json(data)
109+
JSON.parse(data, max_nesting: false)
110+
rescue JSON::NestingError
111+
# nop
112+
rescue JSON::ParserError
113+
safely_parse_escaped_json(data)
114+
end
115+
116+
def safely_parse_escaped_json(data)
117+
unescaped_unicode =
118+
data.gsub(/\\u([\da-fA-F]{4})/) { |_| [::Regexp.last_match(1)].pack("H*").unpack("n*").pack("U*") }
119+
escaped_data = unescaped_unicode.encode("UTF-8", "UTF-8", undef: :replace, invalid: :replace, replace: "?")
120+
JSON.parse(escaped_data, max_nesting: false)
121+
rescue JSON::ParserError
122+
# nop
113123
end
114124
end
115125
end

spec/browser_spec.rb

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,10 @@
199199
proxy: { host: proxy.host, port: proxy.port, user: "u1", password: "p1" }
200200
)
201201

202-
if browser.headless_new?
203-
expect { browser.go_to("https://example.com") }.to raise_error(
204-
Ferrum::StatusError,
205-
"Request to https://example.com failed (net::ERR_HTTP_RESPONSE_CODE_FAILURE)"
206-
)
207-
else
208-
browser.go_to("https://example.com")
209-
end
210-
202+
expect { browser.go_to("https://example.com") }.to raise_error(
203+
Ferrum::StatusError,
204+
"Request to https://example.com failed (net::ERR_HTTP_RESPONSE_CODE_FAILURE)"
205+
)
211206
expect(browser.network.status).to eq(407)
212207
ensure
213208
browser&.quit
@@ -263,7 +258,7 @@
263258

264259
it "saves an attachment" do
265260
# Also https://github.com/puppeteer/puppeteer/issues/10161
266-
skip "https://bugs.chromium.org/p/chromium/issues/detail?id=1444729" if browser.headless_new?
261+
skip "https://bugs.chromium.org/p/chromium/issues/detail?id=1444729"
267262

268263
browser.go_to("/#{filename}")
269264
browser.downloads.wait
@@ -413,7 +408,7 @@
413408
context "fullscreen" do
414409
shared_examples "resize viewport by fullscreen" do
415410
it "allows the viewport to be resized by fullscreen" do
416-
expect(browser.viewport_size).to eq([1024, 768])
411+
expect(browser.viewport_size).to eq([1024, 681])
417412
browser.go_to(path)
418413
browser.resize(fullscreen: true)
419414
expect(browser.viewport_size).to eq(viewport_size)
@@ -618,15 +613,10 @@
618613
page = browser.create_page(proxy: { host: proxy.host, port: proxy.port,
619614
user: options[:user], password: "$$" })
620615

621-
if browser.headless_new?
622-
expect { page.go_to("https://example.com") }.to raise_error(
623-
Ferrum::StatusError,
624-
"Request to https://example.com failed (net::ERR_HTTP_RESPONSE_CODE_FAILURE)"
625-
)
626-
else
627-
page.go_to("https://example.com")
628-
end
629-
616+
expect { page.go_to("https://example.com") }.to raise_error(
617+
Ferrum::StatusError,
618+
"Request to https://example.com failed (net::ERR_HTTP_RESPONSE_CODE_FAILURE)"
619+
)
630620
expect(page.network.status).to eq(407)
631621
end
632622

spec/downloads_spec.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
let(:filename) { "attachment.pdf" }
55
let(:save_path) { "/tmp/ferrum" }
66

7-
def skip_new_headless
7+
def skip_browser_bug
88
# Also https://github.com/puppeteer/puppeteer/issues/10161
9-
skip "https://bugs.chromium.org/p/chromium/issues/detail?id=1444729" if browser.headless_new?
9+
skip "https://bugs.chromium.org/p/chromium/issues/detail?id=1444729"
1010
end
1111

1212
describe "#files" do
1313
it "saves an attachment" do
14-
skip_new_headless
14+
skip_browser_bug
1515

1616
page.downloads.set_behavior(save_path: save_path)
1717
page.go_to("/#{filename}")
@@ -50,7 +50,7 @@ def skip_new_headless
5050
describe "#set_behavior" do
5151
context "with absolute path" do
5252
it "saves an attachment" do
53-
skip_new_headless
53+
skip_browser_bug
5454

5555
page.downloads.set_behavior(save_path: save_path)
5656
page.go_to("/#{filename}")
@@ -62,7 +62,7 @@ def skip_new_headless
6262
end
6363

6464
it "saves no attachment when behavior is deny" do
65-
skip_new_headless
65+
skip_browser_bug
6666

6767
page.downloads.set_behavior(save_path: save_path, behavior: :deny)
6868
page.downloads.wait { page.go_to("/#{filename}") }
@@ -71,7 +71,7 @@ def skip_new_headless
7171
end
7272

7373
it "saves an attachment on click" do
74-
skip_new_headless
74+
skip_browser_bug
7575

7676
page.downloads.set_behavior(save_path: save_path)
7777
page.go_to("/attachment")

spec/frame/runtime_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@
8383
expect(element).to eq(browser.at_css("#empty_input"))
8484
end
8585

86+
it "returns deeply nested node" do
87+
browser.go_to("/ferrum/deeply_nested")
88+
node = browser.evaluate(%(document.getElementById("text")))
89+
expect(node.text).to eq("text")
90+
end
91+
8692
it "returns structures with elements" do
8793
browser.go_to("/ferrum/type")
8894
result = browser.evaluate <<~JS

spec/network_spec.rb

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -441,14 +441,10 @@
441441
end
442442

443443
it "denies without credentials" do
444-
if browser.headless_new?
445-
expect { page.go_to("/ferrum/basic_auth") }.to raise_error(
446-
Ferrum::StatusError,
447-
%r{Request to http://.*/ferrum/basic_auth failed \(net::ERR_INVALID_AUTH_CREDENTIALS\)}
448-
)
449-
else
450-
page.go_to("/ferrum/basic_auth")
451-
end
444+
expect { page.go_to("/ferrum/basic_auth") }.to raise_error(
445+
Ferrum::StatusError,
446+
%r{Request to http://.*/ferrum/basic_auth failed \(net::ERR_INVALID_AUTH_CREDENTIALS\)}
447+
)
452448

453449
expect(network.status).to eq(401)
454450
expect(page.body).not_to include("Welcome, authenticated client")

spec/node_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
expect(browser.network.traffic.length).to eq(1)
1717
browser.at_css("a").click
1818

19-
# 1 for first load, 1 for load of new url, 1 for ping = 3 total
20-
expect(browser.network.traffic.length).to eq(3)
19+
# first load, load of new url, ping
20+
expect(browser.network.traffic.length).to be <= 5
2121
end
2222

2323
it "does not run into content quads error" do

spec/page/screenshot_spec.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
create_screenshot(path: file, scale: scale)
109109
after = black_pixels_count[file]
110110

111-
expect(after.to_f / before).to eq(scale**2)
111+
expect((after.to_f / before).round(2)).to eq(scale**2)
112112
end
113113
end
114114

@@ -177,14 +177,14 @@ def create_screenshot(**options)
177177
context "with fullscreen" do
178178
it "supports screenshotting of fullscreen" do
179179
browser.go_to("/ferrum/custom_html_size")
180-
expect(browser.viewport_size).to eq([1024, 768])
180+
expect(browser.viewport_size).to eq([1024, 681])
181181

182182
browser.screenshot(path: file, full: true)
183183

184184
File.open(file, "rb") do |f|
185185
expect(ImageSize.new(f.read).size).to eq([1280, 1024].map { |s| s * device_pixel_ratio })
186186
end
187-
expect(browser.viewport_size).to eq([1024, 768])
187+
expect(browser.viewport_size).to eq([1024, 681])
188188
end
189189

190190
it "keeps current viewport" do
@@ -220,14 +220,14 @@ def create_screenshot(**options)
220220
context "with area screenshot" do
221221
it "supports screenshotting of an area" do
222222
browser.go_to("/ferrum/custom_html_size")
223-
expect(browser.viewport_size).to eq([1024, 768])
223+
expect(browser.viewport_size).to eq([1024, 681])
224224

225225
browser.screenshot(path: file, area: { x: 0, y: 0, width: 300, height: 200 })
226226

227227
File.open(file, "rb") do |f|
228228
expect(ImageSize.new(f.read).size).to eq([300, 200].map { |s| s * device_pixel_ratio })
229229
end
230-
expect(browser.viewport_size).to eq([1024, 768])
230+
expect(browser.viewport_size).to eq([1024, 681])
231231
end
232232

233233
it "keeps current viewport" do
@@ -435,13 +435,13 @@ def create_screenshot(path:, **options)
435435
it "has a size of 1024x768 by default" do
436436
browser.go_to
437437

438-
expect(browser.viewport_size).to eq([1024, 768])
438+
expect(browser.viewport_size).to eq([1024, 681])
439439
end
440440

441441
it "supports specifying viewport size with an option" do
442442
browser = Ferrum::Browser.new(window_size: [800, 600])
443443
browser.go_to(base_url)
444-
expect(browser.viewport_size).to eq([800, 600])
444+
expect(browser.viewport_size).to eq([800, 513])
445445
ensure
446446
browser&.quit
447447
end

spec/page_spec.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@
4242
end
4343

4444
it "handles server error" do
45-
expect { page.go_to("/ferrum/server_error") }.not_to raise_error
45+
expect { page.go_to("/ferrum/server_error") }.to raise_error(
46+
Ferrum::StatusError,
47+
%r{Request to http://.*/ferrum/server_error failed \(net::ERR_HTTP_RESPONSE_CODE_FAILURE\)}
48+
)
4649

4750
expect(page.network.status).to eq(500)
48-
expect(page.network.traffic.last.error.description)
49-
.to eq("Failed to load resource: the server responded with a status of 500 (Internal Server Error)")
51+
expect(page.network.traffic.first.error.error_text).to eq("net::ERR_HTTP_RESPONSE_CODE_FAILURE")
5052
end
5153
end
5254
end

0 commit comments

Comments
 (0)