class Selenium::WebDriver::DevTools::NetworkInterceptor

Wraps the network request/response interception, providing thread-safety guarantees and handling special cases such as browser canceling requests midst interception.

You should not be using this class directly, use Driver#intercept instead. @api private

Constants

CANNOT_GET_BODY_ON_REDIRECT_ERROR_CODE

CDP fails to get body on certain responses (301) and raises: “Can only get response body on requests captured after headers received.”

INVALID_INTERCEPTION_ID_ERROR_CODE

CDP fails to operate with intercepted requests. Typical reason is browser cancelling intercepted requests/responses.

Attributes

devtools[RW]
lock[RW]

Public Class Methods

new(devtools) click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 43
def initialize(devtools)
  @devtools = devtools
  @lock = Mutex.new
end

Public Instance Methods

intercept(&block) click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 48
def intercept(&block)
  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 Instance Methods

cancelled?(network_id) click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 169
def cancelled?(network_id)
  lock.synchronize { !!cancelled_requests.delete(network_id) }
end
cancelled_requests() click to toggle source

Ensure usage of cancelled_requests is thread-safe via synchronization!

# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 69
def cancelled_requests
  @cancelled_requests ||= []
end
continue_request(id) click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 129
def continue_request(id)
  devtools.fetch.continue_request(request_id: id)
end
Also aliased as: continue_response
continue_response(id)
Alias for: continue_request
fetch_response_body(id) click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 157
def fetch_response_body(id)
  devtools.fetch.get_response_body(request_id: id).dig('result', 'body')
rescue Error::WebDriverError => e
  raise unless e.message.start_with?(CANNOT_GET_BODY_ON_REDIRECT_ERROR_CODE)
end
intercept_request(id, params, &block) click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 99
def intercept_request(id, params, &block)
  original = DevTools::Request.from(id, params)
  mutable = DevTools::Request.from(id, params)

  block.call(mutable) do |&continue| # rubocop:disable Performance/RedundantBlockCall
    pending_response_requests[id] = continue

    if original == mutable
      continue_request(original.id)
    else
      mutate_request(mutable)
    end
  end
end
intercept_response(id, params) { |mutable| ... } click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 114
def intercept_response(id, params)
  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
    continue_response(id)
  else
    mutate_response(mutable)
  end
end
mutate_request(request) click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 134
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
mutate_response(response) click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 146
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
pending_response_requests() click to toggle source

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.

# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 64
def pending_response_requests
  @pending_response_requests ||= {}
end
request_paused(data, &block) click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 79
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
response?(params) click to toggle source

The presence of any of these fields indicate we’re at the response stage. @see chromedevtools.github.io/devtools-protocol/tot/Fetch/#event-requestPaused

# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 95
def response?(params)
  params.key?('responseStatusCode') || params.key?('responseErrorReason')
end
track_cancelled_request(data) click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 73
def track_cancelled_request(data)
  return unless data['canceled']

  lock.synchronize { cancelled_requests << data['requestId'] }
end
with_cancellable_request(network_id) { || ... } click to toggle source
# File lib/selenium/webdriver/devtools/network_interceptor.rb, line 163
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