Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Track cancelled requests so that network interceptor would ignore them
Fixes #10856
  • Loading branch information
p0deje committed Jul 13, 2022
1 parent 5eb4e45 commit 0916b2c
Showing 1 changed file with 93 additions and 34 deletions.
127 changes: 93 additions & 34 deletions rb/lib/selenium/webdriver/devtools/network_interceptor.rb
Expand Up @@ -32,33 +32,70 @@ class DevTools

class NetworkInterceptor

# CDP fails to get body on certain responses (301) and raises:
# "Can only get response body on requests captured after headers received."
CANNOT_GET_BODY_ON_REDIRECT_ERROR_CODE = "-32000"

# CDP fails to operate with intercepted requests.
# Typical reason is browser cancelling intercepted requests/responses.
INVALID_INTERCEPTION_ID_ERROR_CODE = "-32602"

def initialize(devtools)
@devtools = devtools
@lock = Mutex.new
end

def intercept(&block)
devtools.network.set_cache_disabled(cache_disabled: true)

devtools.fetch.on(:request_paused) do |params|
id = params['requestId']
if params.key?('responseStatusCode') || params.key?('responseErrorReason')
intercept_response(id, params, &pending_response_requests.delete(id))
else
intercept_request(id, params, &block)
end
end
devtools.network.on(:loading_failed) { |params| track_cancelled_request(params) }
devtools.fetch.on(:request_paused) { |params| request_paused(params, &block) }

devtools.network.set_cache_disabled(cache_disabled: true)
devtools.network.enable
devtools.fetch.enable(patterns: [{requestStage: 'Request'}, {requestStage: 'Response'}])
end

private

attr_accessor :devtools
attr_accessor :devtools, :lock

# We should be thread-safe to use the hash without synchronization
# because its keys are interception job identifiers and they should be
# unique within a devtools session.
def pending_response_requests
@pending_response_requests ||= {}
end

# Ensure usage of cancelled_requests is thread-safe via synchronization!
def cancelled_requests
@cancelled_requests ||= []
end

def track_cancelled_request(data)
return unless data['canceled']

lock.synchronize { cancelled_requests << data['requestId'] }
end

def request_paused(data, &block)
id = data['requestId']
network_id = data['networkId']

with_cancellable_request(network_id) do
if response?(data)
block = pending_response_requests.delete(id)
intercept_response(id, data, &block)
else
intercept_request(id, data, &block)
end
end
end

# The presence of any of these fields indicate we're at the response stage.
# @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#event-requestPaused
def response?(params)
params.key?('responseStatusCode') || params.key?('responseErrorReason')
end

def intercept_request(id, params, &block)
original = DevTools::Request.from(id, params)
mutable = DevTools::Request.from(id, params)
Expand All @@ -67,48 +104,70 @@ def intercept_request(id, params, &block)
pending_response_requests[id] = continue

if original == mutable
devtools.fetch.continue_request(request_id: id)
continue_request(original.id)
else
devtools.fetch.continue_request(
request_id: id,
url: mutable.url,
method: mutable.method,
post_data: mutable.post_data,
headers: mutable.headers.map do |k, v|
{name: k, value: v}
end
)
mutate_request(mutable)
end
end
end

def intercept_response(id, params)
return devtools.fetch.continue_request(request_id: id) unless block_given?
return continue_response(id) unless block_given?

body = fetch_response_body(id)
original = DevTools::Response.from(id, body, params)
mutable = DevTools::Response.from(id, body, params)
yield mutable

if original == mutable
devtools.fetch.continue_request(request_id: id)
continue_response(id)
else
devtools.fetch.fulfill_request(
request_id: id,
body: (Base64.strict_encode64(mutable.body) if mutable.body),
response_code: mutable.code,
response_headers: mutable.headers.map do |k, v|
{name: k, value: v}
end
)
mutate_response(mutable)
end
end

def continue_request(id)
devtools.fetch.continue_request(request_id: id)
end
alias_method :continue_response, :continue_request

def mutate_request(request)
devtools.fetch.continue_request(
request_id: request.id,
url: request.url,
method: request.method,
post_data: request.post_data,
headers: request.headers.map do |k, v|
{name: k, value: v}
end
)
end

def mutate_response(response)
devtools.fetch.fulfill_request(
request_id: response.id,
body: (Base64.strict_encode64(response.body) if response.body),
response_code: response.code,
response_headers: response.headers.map do |k, v|
{name: k, value: v}
end
)
end

def fetch_response_body(id)
devtools.fetch.get_response_body(request_id: id).dig('result', 'body')
rescue Error::WebDriverError
# CDP fails to get body on certain responses (301) and raises:
# Can only get response body on requests captured after headers received.
rescue Error::WebDriverError => e
raise unless e.message.start_with?(CANNOT_GET_BODY_ON_REDIRECT_ERROR_CODE)
end

def with_cancellable_request(network_id)
yield
rescue Error::WebDriverError => e
raise if e.message.start_with?(INVALID_INTERCEPTION_ID_ERROR_CODE) && !cancelled?(network_id)
end

def cancelled?(network_id)
lock.synchronize { !!cancelled_requests.delete(network_id) }
end

end # NetworkInterceptor
Expand Down

0 comments on commit 0916b2c

Please sign in to comment.