diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c6f90e8..fd2beb1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ ### Changed ### Fixed +- A paused request with a binary body, e.g. a `Blob` upload with `blacklist=`/`whitelist=` set, hung forever: Chrome + sends the body with lone UTF-16 surrogates that the `json` gem rejects, and the fallback that unescaped them broke + the message further, so `Fetch.requestPaused` was dropped and the request was never continued. Lone surrogates are + now replaced with U+FFFD, so broken text from the page reads as `�` instead of `???` [rubycdp/cuprite#308] - Passing a node resolved on the previous page to `evaluate`, e.g. when the page navigates while Cuprite searches within an element, raised `Ferrum::BrowserError: Argument should belong to the same JavaScript world as target object`. It's `Ferrum::NodeNotFoundError` now, so callers can treat the node as stale and find it again diff --git a/lib/ferrum/client/web_socket.rb b/lib/ferrum/client/web_socket.rb index 1069e22f..0ed40b13 100644 --- a/lib/ferrum/client/web_socket.rb +++ b/lib/ferrum/client/web_socket.rb @@ -16,6 +16,7 @@ class WebSocket WEBSOCKET_BUG_SLEEP = 0.05 DEFAULT_PORTS = { "ws" => 80, "wss" => 443 }.freeze SKIP_LOGGING_SCREENSHOTS = !ENV["FERRUM_LOGGING_SCREENSHOTS"] + ESCAPE_OR_LONE_SURROGATE = /\\u[dD][89abAB]\h{2}\\u[dD][c-fC-F]\h{2}|\\u([dD][89a-fA-F]\h{2})|\\./ attr_reader :url, :messages @@ -223,14 +224,12 @@ def safely_parse_json(data) rescue JSON::NestingError # nop rescue JSON::ParserError - safely_parse_escaped_json(data) + safely_parse_lone_surrogates(data) end - def safely_parse_escaped_json(data) - unescaped_unicode = - data.gsub(/\\u([\da-fA-F]{4})/) { |_| [::Regexp.last_match(1)].pack("H*").unpack("n*").pack("U*") } - escaped_data = unescaped_unicode.encode("UTF-8", "UTF-8", undef: :replace, invalid: :replace, replace: "?") - JSON.parse(escaped_data, max_nesting: false) + def safely_parse_lone_surrogates(data) + replaced = data.gsub(ESCAPE_OR_LONE_SURROGATE) { |escape| ::Regexp.last_match(1) ? "\\ufffd" : escape } + JSON.parse(replaced, max_nesting: false) rescue JSON::ParserError # nop end diff --git a/spec/network_spec.rb b/spec/network_spec.rb index d46773c7..8a415120 100644 --- a/spec/network_spec.rb +++ b/spec/network_spec.rb @@ -227,6 +227,18 @@ expect(page.body).not_to include("Disappearing header") end + it "continues requests with a binary body" do + network.blacklist = /jquery/ + page.go_to("/") + + size = page.evaluate_async(<<~JS, page.timeout) + const bytes = new Uint8Array([0x22, 0x5c, 0x00, 0x0a, 0xed, 0xa0, 0x80, 0x41]); + fetch("/upload", { method: "PUT", body: new Blob([bytes]) }).then((r) => r.text()).then(arguments[0]); + JS + + expect(size).to eq("8") + end + it "blocks with single pattern" do network.blacklist = /jquery/ diff --git a/spec/node_spec.rb b/spec/node_spec.rb index 8ef826be..3c86263d 100644 --- a/spec/node_spec.rb +++ b/spec/node_spec.rb @@ -482,7 +482,7 @@ browser.go_to("/unicode") node = browser.at_css("#invalid") - expect(node.text).to eq("Havregrynskake med marengs og nøtter ???") + expect(node.text).to eq("Havregrynskake med marengs og nøtter \uFFFD") end it "works with curly utf8" do diff --git a/spec/support/application.rb b/spec/support/application.rb index 773d8221..0a154fb6 100644 --- a/spec/support/application.rb +++ b/spec/support/application.rb @@ -88,6 +88,10 @@ def authorized?(login, password) "Current host is #{request.scheme}://#{request.host}:#{request.port}" end + put "/upload" do + request.body.read.bytesize.to_s + end + get "/unicode" do File.read("#{FERRUM_VIEWS}/unicode.html") end