puppeteer-ruby 0.45.6 → 0.50.0.alpha5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/AGENTS.md +169 -0
  4. data/CLAUDE/README.md +41 -0
  5. data/CLAUDE/architecture.md +253 -0
  6. data/CLAUDE/cdp_protocol.md +230 -0
  7. data/CLAUDE/concurrency.md +216 -0
  8. data/CLAUDE/porting_puppeteer.md +575 -0
  9. data/CLAUDE/rbs_type_checking.md +101 -0
  10. data/CLAUDE/spec_migration_plans.md +1041 -0
  11. data/CLAUDE/testing.md +278 -0
  12. data/CLAUDE.md +242 -0
  13. data/README.md +8 -0
  14. data/Rakefile +7 -0
  15. data/Steepfile +28 -0
  16. data/docs/api_coverage.md +105 -56
  17. data/lib/puppeteer/aria_query_handler.rb +3 -2
  18. data/lib/puppeteer/async_utils.rb +214 -0
  19. data/lib/puppeteer/browser.rb +98 -56
  20. data/lib/puppeteer/browser_connector.rb +18 -3
  21. data/lib/puppeteer/browser_context.rb +196 -3
  22. data/lib/puppeteer/browser_runner.rb +18 -10
  23. data/lib/puppeteer/cdp_session.rb +67 -23
  24. data/lib/puppeteer/chrome_target_manager.rb +65 -40
  25. data/lib/puppeteer/connection.rb +55 -36
  26. data/lib/puppeteer/console_message.rb +9 -1
  27. data/lib/puppeteer/console_patch.rb +47 -0
  28. data/lib/puppeteer/css_coverage.rb +5 -3
  29. data/lib/puppeteer/custom_query_handler.rb +80 -33
  30. data/lib/puppeteer/define_async_method.rb +31 -37
  31. data/lib/puppeteer/dialog.rb +47 -14
  32. data/lib/puppeteer/element_handle.rb +231 -62
  33. data/lib/puppeteer/emulation_manager.rb +1 -1
  34. data/lib/puppeteer/env.rb +1 -1
  35. data/lib/puppeteer/errors.rb +25 -2
  36. data/lib/puppeteer/event_callbackable.rb +15 -0
  37. data/lib/puppeteer/events.rb +4 -0
  38. data/lib/puppeteer/execution_context.rb +148 -3
  39. data/lib/puppeteer/file_chooser.rb +6 -0
  40. data/lib/puppeteer/frame.rb +162 -91
  41. data/lib/puppeteer/frame_manager.rb +69 -48
  42. data/lib/puppeteer/http_request.rb +114 -38
  43. data/lib/puppeteer/http_response.rb +24 -7
  44. data/lib/puppeteer/isolated_world.rb +64 -41
  45. data/lib/puppeteer/js_coverage.rb +5 -3
  46. data/lib/puppeteer/js_handle.rb +58 -16
  47. data/lib/puppeteer/keyboard.rb +30 -17
  48. data/lib/puppeteer/launcher/browser_options.rb +3 -1
  49. data/lib/puppeteer/launcher/chrome.rb +8 -5
  50. data/lib/puppeteer/launcher/launch_options.rb +7 -2
  51. data/lib/puppeteer/launcher.rb +4 -8
  52. data/lib/puppeteer/lifecycle_watcher.rb +38 -22
  53. data/lib/puppeteer/mouse.rb +273 -64
  54. data/lib/puppeteer/network_event_manager.rb +7 -0
  55. data/lib/puppeteer/network_manager.rb +393 -112
  56. data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
  57. data/lib/puppeteer/page.rb +568 -226
  58. data/lib/puppeteer/puppeteer.rb +171 -64
  59. data/lib/puppeteer/query_handler_manager.rb +112 -16
  60. data/lib/puppeteer/reactor_runner.rb +247 -0
  61. data/lib/puppeteer/remote_object.rb +127 -47
  62. data/lib/puppeteer/target.rb +74 -27
  63. data/lib/puppeteer/task_manager.rb +3 -1
  64. data/lib/puppeteer/timeout_helper.rb +6 -10
  65. data/lib/puppeteer/touch_handle.rb +39 -0
  66. data/lib/puppeteer/touch_screen.rb +72 -22
  67. data/lib/puppeteer/tracing.rb +3 -3
  68. data/lib/puppeteer/version.rb +1 -1
  69. data/lib/puppeteer/wait_task.rb +264 -101
  70. data/lib/puppeteer/web_socket.rb +2 -2
  71. data/lib/puppeteer/web_socket_transport.rb +91 -27
  72. data/lib/puppeteer/web_worker.rb +175 -0
  73. data/lib/puppeteer.rb +20 -4
  74. data/puppeteer-ruby.gemspec +15 -11
  75. data/sig/_external.rbs +8 -0
  76. data/sig/_supplementary.rbs +314 -0
  77. data/sig/puppeteer/browser.rbs +166 -0
  78. data/sig/puppeteer/cdp_session.rbs +64 -0
  79. data/sig/puppeteer/dialog.rbs +41 -0
  80. data/sig/puppeteer/element_handle.rbs +305 -0
  81. data/sig/puppeteer/execution_context.rbs +87 -0
  82. data/sig/puppeteer/frame.rbs +226 -0
  83. data/sig/puppeteer/http_request.rbs +214 -0
  84. data/sig/puppeteer/http_response.rbs +89 -0
  85. data/sig/puppeteer/js_handle.rbs +64 -0
  86. data/sig/puppeteer/keyboard.rbs +40 -0
  87. data/sig/puppeteer/mouse.rbs +113 -0
  88. data/sig/puppeteer/page.rbs +515 -0
  89. data/sig/puppeteer/puppeteer.rbs +98 -0
  90. data/sig/puppeteer/remote_object.rbs +78 -0
  91. data/sig/puppeteer/touch_handle.rbs +21 -0
  92. data/sig/puppeteer/touch_screen.rbs +35 -0
  93. data/sig/puppeteer/web_worker.rbs +83 -0
  94. metadata +116 -45
  95. data/CHANGELOG.md +0 -397
  96. data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
  97. data/lib/puppeteer/firefox_target_manager.rb +0 -157
  98. data/lib/puppeteer/launcher/firefox.rb +0 -453
@@ -1,5 +1,3 @@
1
- require 'timeout'
2
-
3
1
  class Puppeteer::FrameManager
4
2
  include Puppeteer::DebugPrint
5
3
  include Puppeteer::IfPresent
@@ -12,10 +10,11 @@ class Puppeteer::FrameManager
12
10
  # @param {!Puppeteer.Page} page
13
11
  # @param {boolean} ignoreHTTPSErrors
14
12
  # @param {!Puppeteer.TimeoutSettings} timeoutSettings
15
- def initialize(client, page, ignore_https_errors, timeout_settings)
13
+ # @param {boolean} network_enabled
14
+ def initialize(client, page, ignore_https_errors, timeout_settings, network_enabled: true)
16
15
  @client = client
17
16
  @page = page
18
- @network_manager = Puppeteer::NetworkManager.new(client, ignore_https_errors, self)
17
+ @network_manager = Puppeteer::NetworkManager.new(client, ignore_https_errors, self, network_enabled: network_enabled)
19
18
  @timeout_settings = timeout_settings
20
19
 
21
20
  # @type {!Map<string, !Frame>}
@@ -76,17 +75,17 @@ class Puppeteer::FrameManager
76
75
  attr_reader :client, :timeout_settings
77
76
 
78
77
  private def init(target_id, cdp_session = nil)
79
- @frames_pending_target_init[target_id] ||= resolvable_future
78
+ @frames_pending_target_init[target_id] ||= Async::Promise.new
80
79
  client = cdp_session || @client
81
80
 
82
81
  promises = [
83
82
  client.async_send_message('Page.enable'),
84
83
  client.async_send_message('Page.getFrameTree'),
85
84
  ].compact
86
- results = await_all(*promises)
85
+ results = Puppeteer::AsyncUtils.await_promise_all(*promises)
87
86
  frame_tree = results[1]['frameTree']
88
87
  handle_frame_tree(client, frame_tree)
89
- await_all(
88
+ Puppeteer::AsyncUtils.await_promise_all(
90
89
  client.async_send_message('Page.setLifecycleEventsEnabled', enabled: true),
91
90
  client.async_send_message('Runtime.enable'),
92
91
  )
@@ -98,25 +97,28 @@ class Puppeteer::FrameManager
98
97
 
99
98
  raise
100
99
  ensure
101
- @frames_pending_target_init.delete(target_id)&.fulfill(nil)
100
+ @frames_pending_target_init.delete(target_id)&.resolve(nil)
102
101
  end
103
102
 
104
103
  define_async_method :async_init
105
104
 
106
105
  attr_reader :network_manager
107
106
 
108
- class NavigationError < StandardError; end
107
+ class NavigationError < Puppeteer::Error; end
109
108
 
110
109
  # @param frame [Puppeteer::Frame]
111
110
  # @param url [String]
112
- # @param {!{referer?: string, timeout?: number, waitUntil?: string|!Array<string>}=} options
111
+ # @param {!{referer?: string, referrerPolicy?: string, timeout?: number, waitUntil?: string|!Array<string>}=} options
113
112
  # @return [Puppeteer::HTTPResponse]
114
- def navigate_frame(frame, url, referer: nil, timeout: nil, wait_until: nil)
113
+ def navigate_frame(frame, url, referer: nil, referrer_policy: nil, timeout: nil, wait_until: nil)
115
114
  assert_no_legacy_navigation_options(wait_until: wait_until)
116
115
 
116
+ referrer_policy ||= @network_manager.extra_http_headers['referer-policy']
117
+ protocol_referrer_policy = referrer_policy_to_protocol(referrer_policy)
117
118
  navigate_params = {
118
119
  url: url,
119
- referer: referer || @network_manager.extra_http_headers['referer'],
120
+ referrer: referer || @network_manager.extra_http_headers['referer'],
121
+ referrerPolicy: protocol_referrer_policy,
120
122
  frameId: frame.id,
121
123
  }.compact
122
124
  option_wait_until = wait_until || ['load']
@@ -126,20 +128,22 @@ class Puppeteer::FrameManager
126
128
  ensure_new_document_navigation = false
127
129
 
128
130
  begin
129
- navigate = future do
130
- result = @client.send_message('Page.navigate', navigate_params)
131
- loader_id = result['loaderId']
132
- ensure_new_document_navigation = !!loader_id
133
- if result['errorText']
134
- raise NavigationError.new("#{result['errorText']} at #{url}")
135
- end
131
+ navigate = Async do
132
+ Puppeteer::AsyncUtils.future_with_logging do
133
+ result = @client.send_message('Page.navigate', navigate_params)
134
+ loader_id = result['loaderId']
135
+ ensure_new_document_navigation = !!loader_id
136
+ if result['errorText'] && result['errorText'] != 'net::ERR_HTTP_RESPONSE_CODE_FAILURE'
137
+ raise NavigationError.new("#{result['errorText']} at #{url}")
138
+ end
139
+ end.call
136
140
  end
137
- await_any(
141
+ Puppeteer::AsyncUtils.await_promise_race(
138
142
  navigate,
139
143
  watcher.timeout_or_termination_promise,
140
144
  )
141
145
 
142
- await_any(
146
+ Puppeteer::AsyncUtils.await_promise_race(
143
147
  watcher.timeout_or_termination_promise,
144
148
  if ensure_new_document_navigation
145
149
  watcher.new_document_navigation_promise
@@ -150,7 +154,7 @@ class Puppeteer::FrameManager
150
154
 
151
155
  watcher.navigation_response
152
156
  rescue Puppeteer::TimeoutError => err
153
- raise NavigationError.new(err)
157
+ raise err
154
158
  ensure
155
159
  watcher.dispose
156
160
  end
@@ -159,22 +163,29 @@ class Puppeteer::FrameManager
159
163
  # @param timeout [number|nil]
160
164
  # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
161
165
  # @return [Puppeteer::HTTPResponse]
162
- def wait_for_frame_navigation(frame, timeout: nil, wait_until: nil)
166
+ def wait_for_frame_navigation(frame, timeout: nil, wait_until: nil, ignore_same_document_navigation: false)
163
167
  assert_no_legacy_navigation_options(wait_until: wait_until)
164
168
 
165
169
  option_wait_until = wait_until || ['load']
166
170
  option_timeout = timeout || @timeout_settings.navigation_timeout
167
171
  watcher = Puppeteer::LifecycleWatcher.new(self, frame, option_wait_until, option_timeout)
168
172
  begin
169
- await_any(
170
- watcher.timeout_or_termination_promise,
171
- watcher.same_document_navigation_promise,
172
- watcher.new_document_navigation_promise,
173
- )
173
+ if ignore_same_document_navigation
174
+ Puppeteer::AsyncUtils.await_promise_race(
175
+ watcher.timeout_or_termination_promise,
176
+ watcher.new_document_navigation_promise,
177
+ )
178
+ else
179
+ Puppeteer::AsyncUtils.await_promise_race(
180
+ watcher.timeout_or_termination_promise,
181
+ watcher.same_document_navigation_promise,
182
+ watcher.new_document_navigation_promise,
183
+ )
184
+ end
174
185
 
175
186
  watcher.navigation_response
176
187
  rescue Puppeteer::TimeoutError => err
177
- raise NavigationError.new(err)
188
+ raise err
178
189
  ensure
179
190
  watcher.dispose
180
191
  end
@@ -282,10 +293,11 @@ class Puppeteer::FrameManager
282
293
  end
283
294
 
284
295
  if @frames_pending_target_init[parent_frame_id]
285
- @frames_pending_attachment[frame_id] ||= resolvable_future
286
- @frames_pending_target_init[parent_frame_id].then do |_|
296
+ @frames_pending_attachment[frame_id] ||= Async::Promise.new
297
+ Async do
298
+ @frames_pending_target_init[parent_frame_id].wait
287
299
  attach_child_frame(@frames[parent_frame_id], parent_frame_id, frame_id, session)
288
- @frames_pending_attachment.delete(frame_id)&.fulfill(nil)
300
+ @frames_pending_attachment.delete(frame_id)&.resolve(nil)
289
301
  end
290
302
  return
291
303
  end
@@ -293,7 +305,7 @@ class Puppeteer::FrameManager
293
305
  raise FrameNotFoundError.new("Parent frame #{parent_frame_id} not found.")
294
306
  end
295
307
 
296
- class FrameNotFoundError < StandardError ; end
308
+ class FrameNotFoundError < Puppeteer::Error ; end
297
309
 
298
310
  private def attach_child_frame(parent_frame, parent_frame_id, frame_id, session)
299
311
  unless parent_frame
@@ -313,7 +325,8 @@ class Puppeteer::FrameManager
313
325
 
314
326
 
315
327
  if @frames_pending_attachment[frame_id]
316
- @frames_pending_attachment[frame_id].then do |_|
328
+ Async do
329
+ @frames_pending_attachment[frame_id].wait
317
330
  frame = is_main_frame ? @main_frame : @frames[frame_id]
318
331
  reattach_frame(frame, frame_id, is_main_frame, frame_payload)
319
332
  end
@@ -360,7 +373,6 @@ class Puppeteer::FrameManager
360
373
  private def ensure_isolated_world(session, name)
361
374
  key = "#{session.id}:#{name}"
362
375
  return if @isolated_worlds.include?(key)
363
- @isolated_worlds << key
364
376
 
365
377
  session.send_message('Page.addScriptToEvaluateOnNewDocument',
366
378
  source: "//# sourceURL=#{Puppeteer::ExecutionContext::EVALUATION_SCRIPT_URL}",
@@ -369,13 +381,18 @@ class Puppeteer::FrameManager
369
381
  create_isolated_worlds_promises = frames.
370
382
  select { |frame| frame._client == session }.
371
383
  map do |frame|
372
- session.async_send_message('Page.createIsolatedWorld',
373
- frameId: frame.id,
374
- grantUniveralAccess: true,
375
- worldName: name,
376
- )
384
+ Async do
385
+ session.send_message('Page.createIsolatedWorld',
386
+ frameId: frame.id,
387
+ grantUniveralAccess: true,
388
+ worldName: name,
389
+ )
390
+ rescue => err
391
+ debug_puts(err)
392
+ end
377
393
  end
378
- await_all(*create_isolated_worlds_promises)
394
+ Puppeteer::AsyncUtils.await_promise_all(*create_isolated_worlds_promises)
395
+ @isolated_worlds << key
379
396
  end
380
397
 
381
398
  # @param frame_id [String]
@@ -445,22 +462,20 @@ class Puppeteer::FrameManager
445
462
  context = @context_id_to_context[key]
446
463
  return unless context
447
464
  @context_id_to_context.delete(key)
448
- if context.world
449
- context.world.delete_context(execution_context_id)
450
- end
465
+ context.world&.delete_context(context)
451
466
  end
452
467
 
453
468
  # @param session [Puppeteer::CDPSession]
454
469
  def handle_execution_contexts_cleared(session)
470
+ session_id = session.id
455
471
  @context_id_to_context.select! do |execution_context_id, context|
472
+ key_session_id, context_id = execution_context_id.split(':', 2)
456
473
  # Make sure to only clear execution contexts that belong
457
474
  # to the current session.
458
- if context.client != session
475
+ if key_session_id != session_id
459
476
  true # keep
460
477
  else
461
- if context.world
462
- context.world.delete_context(execution_context_id)
463
- end
478
+ context.world&.delete_context(context)
464
479
  false # remove
465
480
  end
466
481
  end
@@ -481,6 +496,12 @@ class Puppeteer::FrameManager
481
496
  emit_event(FrameManagerEmittedEvents::FrameDetached, frame)
482
497
  end
483
498
 
499
+ private def referrer_policy_to_protocol(referrer_policy)
500
+ return nil if referrer_policy.nil?
501
+
502
+ referrer_policy.to_s.gsub(/-([a-z])/) { Regexp.last_match(1).upcase }
503
+ end
504
+
484
505
  private def assert_no_legacy_navigation_options(wait_until:)
485
506
  if wait_until == 'networkidle'
486
507
  raise ArgumentError.new('ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead')
@@ -1,3 +1,5 @@
1
+ # rbs_inline: enabled
2
+
1
3
  class Puppeteer::HTTPRequest
2
4
  include Puppeteer::DebugPrint
3
5
  include Puppeteer::IfPresent
@@ -14,6 +16,10 @@ class Puppeteer::HTTPRequest
14
16
  @request.instance_variable_get(:@request_id)
15
17
  end
16
18
 
19
+ def client=(client)
20
+ @request.instance_variable_set(:@client, client)
21
+ end
22
+
17
23
  def interception_id
18
24
  @request.instance_variable_get(:@interception_id)
19
25
  end
@@ -90,27 +96,55 @@ class Puppeteer::HTTPRequest
90
96
  @interception_id = interception_id
91
97
  @allow_interception = allow_interception
92
98
  @url = event['request']['url']
93
- @resource_type = event['type'].downcase
99
+ url_fragment = event.dig('request', 'urlFragment')
100
+ @url += url_fragment if url_fragment
101
+ resource_type = event['type'] || event['resourceType'] || 'other'
102
+ @resource_type = resource_type.downcase
94
103
  @method = event['request']['method']
95
104
  @post_data = event['request']['postData']
105
+ has_post_data = event.dig('request', 'hasPostData')
106
+ @has_post_data = has_post_data.nil? ? !@post_data.nil? : has_post_data
96
107
  @frame = frame
97
108
  @redirect_chain = redirect_chain
98
109
  @continue_request_overrides = {}
99
110
  @intercept_resolution_state = InterceptResolutionState.none
111
+ @interception_handled = false
100
112
  @intercept_handlers = []
101
113
  @initiator = event['initiator']
102
114
 
103
115
  @headers = {}
104
- event['request']['headers'].each do |key, value|
105
- @headers[key.downcase] = value
106
- end
116
+ update_headers(event.dig('request', 'headers') || {})
107
117
  @from_memory_cache = false
108
118
 
109
119
  @internal = InternalAccessor.new(self)
110
120
  end
111
121
 
112
122
  attr_reader :internal
113
- attr_reader :url, :resource_type, :method, :post_data, :headers, :response, :frame, :initiator
123
+ attr_reader :client, :url, :resource_type, :method, :post_data, :response, :frame, :initiator
124
+
125
+ def update_headers(headers)
126
+ headers.each do |key, value|
127
+ @headers[key.downcase] = value
128
+ end
129
+ end
130
+
131
+ def headers
132
+ @headers.dup
133
+ end
134
+
135
+ # @rbs return: bool -- Whether request has post data
136
+ def has_post_data?
137
+ @has_post_data
138
+ end
139
+
140
+ # @rbs return: String? -- Post data string if available
141
+ def fetch_post_data
142
+ response = @client.send_message('Network.getRequestPostData', requestId: @request_id)
143
+ response['postData']
144
+ rescue => err
145
+ debug_puts(err)
146
+ nil
147
+ end
114
148
 
115
149
  def inspect
116
150
  values = %i[request_id method url].map do |sym|
@@ -132,6 +166,10 @@ class Puppeteer::HTTPRequest
132
166
  end
133
167
  end
134
168
 
169
+ private def can_be_intercepted?
170
+ !@url.start_with?('data:') && !@from_memory_cache
171
+ end
172
+
135
173
  # @returns the `ContinueRequestOverrides` that will be used
136
174
  # if the interception is allowed to continue (ie, `abort()` and
137
175
  # `respond()` aren't called).
@@ -189,11 +227,15 @@ class Puppeteer::HTTPRequest
189
227
  # Awaits pending interception handlers and then decides how to fulfill
190
228
  # the request interception.
191
229
  def finalize_interceptions
192
- @intercept_handlers.each(&:call)
230
+ @intercept_handlers.each do |handler|
231
+ Puppeteer::AsyncUtils.await(handler.call)
232
+ end
233
+ @intercept_handlers = []
193
234
  case intercept_resolution_state.action
194
235
  when 'abort'
195
- abort_impl(**@abort_error_reason)
236
+ abort_impl(@abort_error_reason)
196
237
  when 'respond'
238
+ raise "Response is missing for the interception" if @response_for_request.nil?
197
239
  respond_impl(**@response_for_request)
198
240
  when 'continue'
199
241
  continue_impl(@continue_request_overrides)
@@ -218,23 +260,26 @@ class Puppeteer::HTTPRequest
218
260
  return nil unless headers
219
261
 
220
262
  headers.flat_map do |key, value|
221
- if value.is_a?(Enumerable)
222
- value.map do |v|
223
- { name: key, value: v.to_s }
263
+ next [] if value.nil?
264
+
265
+ name = key.to_s
266
+ if value.is_a?(Array)
267
+ value.compact.map do |v|
268
+ { name: name, value: v.to_s }
224
269
  end
225
270
  else
226
- { name: key, value: value.to_s }
271
+ { name: name, value: value.to_s }
227
272
  end
228
273
  end
229
274
  end
230
275
 
231
- class InterceptionNotEnabledError < StandardError
276
+ class InterceptionNotEnabledError < Puppeteer::Error
232
277
  def initialize
233
278
  super('Request Interception is not enabled!')
234
279
  end
235
280
  end
236
281
 
237
- class AlreadyHandledError < StandardError
282
+ class AlreadyHandledError < Puppeteer::Error
238
283
  def initialize
239
284
  super('Request is already handled!')
240
285
  end
@@ -255,17 +300,15 @@ class Puppeteer::HTTPRequest
255
300
  #
256
301
  # @param error_code [String|Symbol]
257
302
  def continue(url: nil, method: nil, post_data: nil, headers: nil, priority: nil)
258
- # Request interception is not supported for data: urls.
259
- return if @url.start_with?('data:')
260
-
261
303
  assert_interception_allowed
262
304
  assert_interception_not_handled
305
+ return unless can_be_intercepted?
263
306
 
264
307
  overrides = {
265
308
  url: url,
266
309
  method: method,
267
- postData: post_data,
268
- headers: headers_to_array(headers),
310
+ post_data: post_data,
311
+ headers: headers,
269
312
  }.compact
270
313
 
271
314
  if priority.nil?
@@ -280,7 +323,7 @@ class Puppeteer::HTTPRequest
280
323
  end
281
324
 
282
325
  if priority == @intercept_resolution_state.priority
283
- if @intercept_resolution_state.action == :abort || @intercept_resolution_state.action == :respond
326
+ if @intercept_resolution_state.action == 'abort' || @intercept_resolution_state.action == 'respond'
284
327
  return
285
328
  end
286
329
  @intercept_resolution_state = InterceptResolutionState.continue(priority: priority)
@@ -291,14 +334,24 @@ class Puppeteer::HTTPRequest
291
334
  @interception_handled = true
292
335
 
293
336
  begin
337
+ raise Puppeteer::Error.new('HTTPRequest is missing interception id needed for Fetch.continueRequest') if @interception_id.nil?
338
+
339
+ post_data = overrides[:post_data]
340
+ overrides = overrides.merge(
341
+ postData: post_data ? Base64.strict_encode64(post_data.b) : nil,
342
+ headers: headers_to_array(overrides[:headers]),
343
+ ).compact
344
+ overrides.delete(:post_data)
345
+
294
346
  @client.send_message('Fetch.continueRequest',
295
347
  requestId: @interception_id,
296
348
  **overrides,
297
349
  )
298
350
  rescue => err
351
+ @interception_handled = false unless target_closed_error?(err)
299
352
  # In certain cases, protocol will return error if the request was already canceled
300
353
  # or the page was closed. We should tolerate these errors.
301
- debug_puts(err)
354
+ handle_interception_error(err)
302
355
  end
303
356
  end
304
357
 
@@ -319,11 +372,9 @@ class Puppeteer::HTTPRequest
319
372
  # @param content_type [String]
320
373
  # @param body [String]
321
374
  def respond(status: nil, headers: nil, content_type: nil, body: nil, priority: nil)
322
- # Mocking responses for dataURL requests is not currently supported.
323
- return if @url.start_with?('data:')
324
-
325
375
  assert_interception_allowed
326
376
  assert_interception_not_handled
377
+ return unless can_be_intercepted?
327
378
 
328
379
  if priority.nil?
329
380
  respond_impl(status: status, headers: headers, content_type: content_type, body: body)
@@ -335,7 +386,7 @@ class Puppeteer::HTTPRequest
335
386
  headers: headers,
336
387
  content_type: content_type,
337
388
  body: body,
338
- }
389
+ }.compact
339
390
 
340
391
  if @intercept_resolution_state.priority_unspecified? || priority > @intercept_resolution_state.priority
341
392
  @intercept_resolution_state = InterceptResolutionState.respond(priority: priority)
@@ -343,7 +394,7 @@ class Puppeteer::HTTPRequest
343
394
  end
344
395
 
345
396
  if priority == @intercept_resolution_state.priority
346
- if @intercept_resolution_state.action == :abort
397
+ if @intercept_resolution_state.action == 'abort'
347
398
  return
348
399
  end
349
400
  @intercept_resolution_state = InterceptResolutionState.respond(priority: priority)
@@ -353,10 +404,18 @@ class Puppeteer::HTTPRequest
353
404
  private def respond_impl(status: nil, headers: nil, content_type: nil, body: nil)
354
405
  @interception_handled = true
355
406
 
407
+ parsed_body = if body
408
+ binary = body.to_s.b
409
+ {
410
+ content_length: binary.bytesize,
411
+ base64: Base64.strict_encode64(binary),
412
+ }
413
+ end
414
+
356
415
  mock_response_headers = {}
357
416
  headers&.each do |key, value|
358
- mock_response_headers[key.downcase] =
359
- if value.is_a?(Enumerable)
417
+ mock_response_headers[key.to_s.downcase] =
418
+ if value.is_a?(Array)
360
419
  value.map(&:to_s)
361
420
  else
362
421
  value.to_s
@@ -365,26 +424,29 @@ class Puppeteer::HTTPRequest
365
424
  if content_type
366
425
  mock_response_headers['content-type'] = content_type
367
426
  end
368
- if body
369
- mock_response_headers['content-length'] = body.length
427
+ if parsed_body && parsed_body[:content_length] > 0 && !mock_response_headers.key?('content-length')
428
+ mock_response_headers['content-length'] = parsed_body[:content_length].to_s
370
429
  end
371
430
 
372
431
  mock_response = {
373
432
  responseCode: status || 200,
374
433
  responsePhrase: STATUS_TEXTS[(status || 200).to_s],
375
434
  responseHeaders: headers_to_array(mock_response_headers),
376
- body: if_present(body) { |mock_body| Base64.strict_encode64(mock_body) },
435
+ body: parsed_body && parsed_body[:base64],
377
436
  }.compact
378
437
 
379
438
  begin
439
+ raise Puppeteer::Error.new('HTTPRequest is missing interception id needed for Fetch.fulfillRequest') if @interception_id.nil?
440
+
380
441
  @client.send_message('Fetch.fulfillRequest',
381
442
  requestId: @interception_id,
382
443
  **mock_response,
383
444
  )
384
445
  rescue => err
446
+ @interception_handled = false unless target_closed_error?(err)
385
447
  # In certain cases, protocol will return error if the request was already canceled
386
448
  # or the page was closed. We should tolerate these errors.
387
- debug_puts(err)
449
+ handle_interception_error(err)
388
450
  end
389
451
  end
390
452
 
@@ -402,22 +464,20 @@ class Puppeteer::HTTPRequest
402
464
  #
403
465
  # @param error_code [String|Symbol]
404
466
  def abort(error_code: :failed, priority: nil)
405
- # Request interception is not supported for data: urls.
406
- return if @url.start_with?('data:')
407
-
408
467
  error_reason = ERROR_REASONS[error_code.to_s]
409
468
  unless error_reason
410
469
  raise ArgumentError.new("Unknown error code: #{error_code}")
411
470
  end
412
471
  assert_interception_allowed
413
472
  assert_interception_not_handled
473
+ return unless can_be_intercepted?
414
474
 
415
475
  if priority.nil?
416
- abort_impl(error_reason)
476
+ return abort_impl(error_reason)
417
477
  end
418
478
  @abort_error_reason = error_reason
419
479
 
420
- if @intercept_resolution_state.priority_unspecified? || priority > @intercept_resolution_state.priority
480
+ if @intercept_resolution_state.priority_unspecified? || priority >= @intercept_resolution_state.priority
421
481
  @intercept_resolution_state = InterceptResolutionState.abort(priority: priority)
422
482
  end
423
483
  end
@@ -426,15 +486,31 @@ class Puppeteer::HTTPRequest
426
486
  @interception_handled = true
427
487
 
428
488
  begin
489
+ raise Puppeteer::Error.new('HTTPRequest is missing interception id needed for Fetch.failRequest') if @interception_id.nil?
490
+
429
491
  @client.send_message('Fetch.failRequest',
430
492
  requestId: @interception_id,
431
- errorReason: error_reason,
493
+ errorReason: error_reason || ERROR_REASONS.fetch('failed'),
432
494
  )
433
495
  rescue => err
496
+ @interception_handled = false unless target_closed_error?(err)
434
497
  # In certain cases, protocol will return error if the request was already canceled
435
498
  # or the page was closed. We should tolerate these errors.
436
- debug_puts(err)
499
+ handle_interception_error(err)
500
+ end
501
+ end
502
+
503
+ private def target_closed_error?(error)
504
+ message = error.message.to_s
505
+ message.match?(/Target closed|Session closed|Connection closed/i)
506
+ end
507
+
508
+ private def handle_interception_error(error)
509
+ message = error.message.to_s
510
+ if message.match?(/Invalid header|Unsafe header|Expected "header"|invalid argument/i)
511
+ raise error
437
512
  end
513
+ debug_puts(error)
438
514
  end
439
515
 
440
516
  ERROR_REASONS = {