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,3 +1,5 @@
1
+ require 'async/semaphore'
2
+
1
3
  class Puppeteer::NetworkManager
2
4
  include Puppeteer::DebugPrint
3
5
  include Puppeteer::EventCallbackable
@@ -16,8 +18,8 @@ class Puppeteer::NetworkManager
16
18
  class InternalNetworkCondition
17
19
  attr_writer :offline, :upload, :download, :latency
18
20
 
19
- def initialize(client)
20
- @client = client
21
+ def initialize(sender)
22
+ @sender = sender
21
23
  @offline = false
22
24
  @upload = -1
23
25
  @download = -1
@@ -43,13 +45,21 @@ class Puppeteer::NetworkManager
43
45
  update_network_conditions
44
46
  end
45
47
 
46
- private def update_network_conditions
47
- @client.send_message('Network.emulateNetworkConditions',
48
+ def params
49
+ {
48
50
  offline: @offline,
49
51
  latency: @latency,
50
52
  downloadThroughput: @download,
51
53
  uploadThroughput: @upload,
52
- )
54
+ }
55
+ end
56
+
57
+ def refresh
58
+ update_network_conditions
59
+ end
60
+
61
+ private def update_network_conditions
62
+ @sender.call('Network.emulateNetworkConditions', params)
53
63
  end
54
64
  end
55
65
 
@@ -72,50 +82,51 @@ class Puppeteer::NetworkManager
72
82
  # @param {!Puppeteer.CDPSession} client
73
83
  # @param {boolean} ignoreHTTPSErrors
74
84
  # @param {!Puppeteer.FrameManager} frameManager
75
- def initialize(client, ignore_https_errors, frame_manager)
85
+ # @param {boolean} network_enabled
86
+ def initialize(client, ignore_https_errors, frame_manager, network_enabled: true)
76
87
  @client = client
77
88
  @ignore_https_errors = ignore_https_errors
78
89
  @frame_manager = frame_manager
90
+ @network_enabled = network_enabled
79
91
  @network_event_manager = Puppeteer::NetworkEventManager.new
92
+ @clients = Set.new
93
+ @initialized = false
80
94
 
81
95
  @extra_http_headers = {}
96
+ @user_agent = nil
97
+ @user_agent_metadata = nil
82
98
 
83
99
  @attempted_authentications = Set.new
84
100
  @user_request_interception_enabled = false
85
101
  @protocol_request_interception_enabled = false
86
- @user_cache_disabled = false
87
- @internal_network_condition = InternalNetworkCondition.new(@client)
102
+ @user_cache_disabled = nil
103
+ @internal_network_condition = InternalNetworkCondition.new(method(:send_to_clients))
104
+ @interception_semaphore = Async::Semaphore.new(1)
88
105
 
89
- @client.on_event('Fetch.requestPaused') do |event|
90
- handle_request_paused(event)
91
- end
92
- @client.on_event('Fetch.authRequired') do |event|
93
- handle_auth_required(event)
94
- end
95
- @client.on_event('Network.requestWillBeSent') do |event|
96
- handle_request_will_be_sent(event)
97
- end
98
- @client.on_event('Network.requestServedFromCache') do |event|
99
- handle_request_served_from_cache(event)
100
- end
101
- @client.on_event('Network.responseReceived') do |event|
102
- handle_response_received(event)
103
- end
104
- @client.on_event('Network.loadingFinished') do |event|
105
- handle_loading_finished(event)
106
- end
107
- @client.on_event('Network.loadingFailed') do |event|
108
- handle_loading_failed(event)
109
- end
110
- @client.on_event('Network.responseReceivedExtraInfo') do |event|
111
- handle_response_received_extra_info(event)
112
- end
106
+ add_client(@client)
113
107
  end
114
108
 
115
109
  def init
116
- @client.send_message('Network.enable')
117
- if @ignore_https_errors
118
- @client.send_message('Security.setIgnoreCertificateErrors', ignore: true)
110
+ apply_to_clients do |client|
111
+ configure_client(client)
112
+ end
113
+ @initialized = true
114
+ end
115
+
116
+ def add_client(client)
117
+ return unless @network_enabled
118
+ return if @clients.include?(client)
119
+
120
+ @clients << client
121
+ setup_listeners(client)
122
+ if @initialized
123
+ if Async::Task.current?
124
+ Async do
125
+ configure_client(client)
126
+ end
127
+ else
128
+ configure_client(client)
129
+ end
119
130
  end
120
131
  end
121
132
 
@@ -127,10 +138,87 @@ class Puppeteer::NetworkManager
127
138
  "#<Puppeteer::HTTPRequest #{values.join(' ')}>"
128
139
  end
129
140
 
141
+ private def apply_to_clients
142
+ @clients.each do |client|
143
+ yield client
144
+ end
145
+ end
146
+
147
+ private def ignore_client_error?(error)
148
+ message = error&.message
149
+ return false unless message
150
+
151
+ lowered = message.downcase
152
+ return true if lowered.include?('target closed')
153
+ return true if lowered.include?('session closed')
154
+ return true if lowered.include?('not supported')
155
+ return true if lowered.include?("wasn't found")
156
+
157
+ false
158
+ end
159
+
160
+ private def safe_send_message(client, method, params = {})
161
+ client.send_message(method, params)
162
+ rescue => err
163
+ raise unless ignore_client_error?(err)
164
+ end
165
+
166
+ private def send_to_clients(method, params)
167
+ apply_to_clients do |client|
168
+ safe_send_message(client, method, params)
169
+ end
170
+ end
171
+
172
+ private def apply_extra_http_headers(client)
173
+ safe_send_message(client, 'Network.setExtraHTTPHeaders', headers: @extra_http_headers)
174
+ end
175
+
176
+ private def apply_user_agent(client)
177
+ return unless @user_agent
178
+
179
+ safe_send_message(client, 'Network.setUserAgentOverride', {
180
+ userAgent: @user_agent,
181
+ userAgentMetadata: @user_agent_metadata,
182
+ }.compact)
183
+ end
184
+
185
+ private def apply_protocol_cache_disabled(client)
186
+ return if @user_cache_disabled.nil?
187
+
188
+ safe_send_message(client, 'Network.setCacheDisabled', cacheDisabled: @user_cache_disabled)
189
+ end
190
+
191
+ private def apply_protocol_request_interception(client)
192
+ if @protocol_request_interception_enabled
193
+ safe_send_message(client, 'Fetch.enable',
194
+ handleAuthRequests: true,
195
+ patterns: [{ urlPattern: '*' }],
196
+ )
197
+ else
198
+ safe_send_message(client, 'Fetch.disable')
199
+ end
200
+ end
201
+
202
+ private def configure_client(client)
203
+ safe_send_message(client, 'Network.enable')
204
+ if @ignore_https_errors
205
+ safe_send_message(client, 'Security.setIgnoreCertificateErrors', ignore: true)
206
+ end
207
+ apply_extra_http_headers(client)
208
+ apply_user_agent(client)
209
+ apply_protocol_cache_disabled(client)
210
+ apply_protocol_request_interception(client)
211
+ safe_send_message(client, 'Network.emulateNetworkConditions', @internal_network_condition.params)
212
+ end
213
+
130
214
  # @param username [String|NilClass]
131
215
  # @param password [String|NilClass]
132
216
  def authenticate(username:, password:)
133
- @credentials = Credentials.new(username: username, password: password)
217
+ if username.nil? && password.nil?
218
+ @credentials = nil
219
+ else
220
+ @credentials = Credentials.new(username: username, password: password)
221
+ end
134
222
  update_protocol_request_interception
135
223
  end
136
224
 
@@ -139,12 +227,27 @@ class Puppeteer::NetworkManager
139
227
  new_extra_http_headers = {}
140
228
  headers.each do |key, value|
141
229
  unless value.is_a?(String)
142
- raise ArgumentError.new("Expected value of header \"#{key}\" to be String, but \"#{value}\" is found.")
230
+ type_description =
231
+ case value
232
+ when Numeric
233
+ 'number'
234
+ when TrueClass, FalseClass
235
+ 'boolean'
236
+ when NilClass
237
+ 'null'
238
+ when Symbol
239
+ 'symbol'
240
+ when Array, Hash
241
+ 'object'
242
+ else
243
+ value.class.to_s
244
+ end
245
+ raise ArgumentError.new("Expected value of header \"#{key}\" to be String, but \"#{type_description}\" is found.")
143
246
  end
144
247
  new_extra_http_headers[key.downcase] = value
145
248
  end
146
249
  @extra_http_headers = new_extra_http_headers
147
- @client.send_message('Network.setExtraHTTPHeaders', headers: new_extra_http_headers)
250
+ apply_to_clients { |client| apply_extra_http_headers(client) }
148
251
  end
149
252
 
150
253
  # @return {!Object<string, string>}
@@ -169,10 +272,9 @@ class Puppeteer::NetworkManager
169
272
  # @param user_agent [String]
170
273
  # @param user_agent_metadata [Hash]
171
274
  def set_user_agent(user_agent, user_agent_metadata = nil)
172
- @client.send_message('Network.setUserAgentOverride', {
173
- userAgent: user_agent,
174
- userAgentMetadata: user_agent_metadata,
175
- }.compact)
275
+ @user_agent = user_agent
276
+ @user_agent_metadata = user_agent_metadata
277
+ apply_to_clients { |client| apply_user_agent(client) }
176
278
  end
177
279
  alias_method :user_agent=, :set_user_agent
178
280
 
@@ -186,61 +288,106 @@ class Puppeteer::NetworkManager
186
288
  update_protocol_request_interception
187
289
  end
188
290
 
291
+ private def setup_listeners(client)
292
+ client.add_event_listener(CDPSessionEmittedEvents::Disconnected) do
293
+ @clients.delete(client)
294
+ end
295
+
296
+ client.on_event('Fetch.requestPaused') do |event|
297
+ handle_request_paused(event, client)
298
+ end
299
+ client.on_event('Fetch.authRequired') do |event|
300
+ handle_auth_required(event, client)
301
+ end
302
+ client.on_event('Network.requestWillBeSent') do |event|
303
+ handle_request_will_be_sent(event, client)
304
+ end
305
+ client.on_event('Network.requestWillBeSentExtraInfo') do |event|
306
+ handle_request_will_be_sent_extra_info(event, client)
307
+ end
308
+ client.on_event('Network.requestServedFromCache') do |event|
309
+ handle_request_served_from_cache(event, client)
310
+ end
311
+ client.on_event('Network.responseReceived') do |event|
312
+ handle_response_received(event, client)
313
+ end
314
+ client.on_event('Network.loadingFinished') do |event|
315
+ handle_loading_finished(event, client)
316
+ end
317
+ client.on_event('Network.loadingFailed') do |event|
318
+ handle_loading_failed(event, client)
319
+ end
320
+ client.on_event('Network.responseReceivedExtraInfo') do |event|
321
+ handle_response_received_extra_info(event, client)
322
+ end
323
+ end
324
+
189
325
  private def update_protocol_request_interception
190
326
  enabled = @user_request_interception_enabled || !@credentials.nil?
191
327
  return if @protocol_request_interception_enabled == enabled
192
328
  @protocol_request_interception_enabled = enabled
193
329
 
194
- if enabled
195
- update_protocol_cache_disabled
196
- @client.send_message('Fetch.enable',
197
- handleAuthRequests: true,
198
- patterns: [{ urlPattern: '*' }],
199
- )
200
- else
201
- update_protocol_cache_disabled
202
- @client.async_send_message('Fetch.disable')
203
- end
330
+ @user_cache_disabled = false if @user_cache_disabled.nil?
331
+
332
+ update_protocol_cache_disabled
333
+ apply_to_clients { |client| apply_protocol_request_interception(client) }
204
334
  end
205
335
 
206
336
  private def update_protocol_cache_disabled
207
- cache_disabled = @user_cache_disabled || @protocol_request_interception_enabled
208
- @client.send_message('Network.setCacheDisabled', cacheDisabled: cache_disabled)
337
+ return if @user_cache_disabled.nil?
338
+
339
+ apply_to_clients { |client| apply_protocol_cache_disabled(client) }
209
340
  end
210
341
 
211
- private def handle_request_will_be_sent(event)
342
+ private def handle_request_will_be_sent(event, client)
343
+ network_request_id = event['requestId']
344
+ event_url = event.dig('request', 'url')
345
+ if event_url
346
+ url_fragment = event.dig('request', 'urlFragment')
347
+ event_url += url_fragment if url_fragment
348
+ end
349
+ existing_request = @network_event_manager.get_request(network_request_id)
350
+ if existing_request &&
351
+ existing_request.url == event_url &&
352
+ existing_request.method == event.dig('request', 'method')
353
+ if_present(@network_event_manager.request_extra_info(network_request_id).shift) do |extra_info|
354
+ existing_request.update_headers(extra_info['headers'])
355
+ end
356
+ return
357
+ end
358
+
212
359
  # Request interception doesn't happen for data URLs with Network Service.
213
360
  if @user_request_interception_enabled && !event['request']['url'].start_with?('data:')
214
- network_request_id = event['requestId']
215
361
  @network_event_manager.store_request_will_be_sent(network_request_id, event)
216
362
 
217
363
  # CDP may have sent a Fetch.requestPaused event already. Check for it.
218
364
  if_present(@network_event_manager.get_request_paused(network_request_id)) do |request_paused_event|
219
365
  fetch_request_id = request_paused_event['requestId']
220
366
  patch_request_event_headers(event, request_paused_event)
221
- handle_request(event, fetch_request_id)
367
+ handle_request(event, fetch_request_id, client: client)
222
368
  @network_event_manager.forget_request_paused(network_request_id)
223
369
  end
224
370
 
225
371
  return
226
372
  end
227
- handle_request(event, nil)
373
+ handle_request(event, nil, client: client)
228
374
  end
229
375
 
230
- private def handle_auth_required(event)
376
+ private def handle_auth_required(event, client)
377
+ auth_request_id = event['requestId']
231
378
  response = 'Default'
232
- if @attempted_authentications.include?(event['requestId'])
379
+ if @attempted_authentications.include?(auth_request_id)
233
380
  response = 'CancelAuth'
234
381
  elsif @credentials
235
382
  response = 'ProvideCredentials'
236
- @attempted_authentications << event['requestId']
383
+ @attempted_authentications << auth_request_id
237
384
  end
238
385
 
239
386
  username = @credentials&.username
240
387
  password = @credentials&.password
241
388
 
242
389
  begin
243
- @client.send_message('Fetch.continueWithAuth',
390
+ client.send_message('Fetch.continueWithAuth',
244
391
  requestId: event['requestId'],
245
392
  authChallengeResponse: {
246
393
  response: response,
@@ -253,18 +400,22 @@ class Puppeteer::NetworkManager
253
400
  end
254
401
  end
255
402
 
256
- private def handle_request_paused(event)
403
+ private def handle_request_paused(event, client)
257
404
  if !@user_request_interception_enabled && @protocol_request_interception_enabled
258
405
  begin
259
- @client.send_message('Fetch.continueRequest', requestId: event['requestId'])
406
+ client.send_message('Fetch.continueRequest', requestId: event['requestId'])
260
407
  rescue => err
261
408
  debug_puts(err)
262
409
  end
263
410
  end
264
411
 
265
- network_request_id = event['networkId']
412
+ request_id = event['networkId']
266
413
  fetch_request_id = event['requestId']
267
- return unless network_request_id
414
+ if !request_id || request_id.empty?
415
+ handle_request_without_network_instrumentation(event, client)
416
+ return
417
+ end
418
+ network_request_id = request_id
268
419
 
269
420
  request_will_be_sent_event = @network_event_manager.get_request_will_be_sent(network_request_id)
270
421
 
@@ -279,9 +430,16 @@ class Puppeteer::NetworkManager
279
430
 
280
431
  if request_will_be_sent_event
281
432
  patch_request_event_headers(request_will_be_sent_event, event)
282
- handle_request(request_will_be_sent_event, fetch_request_id)
433
+ handle_request(request_will_be_sent_event, fetch_request_id, client: client)
283
434
  else
284
- @network_event_manager.store_request_paused(network_request_id, event)
435
+ if event['redirectedRequestId']
436
+ handle_redirect_request_paused(event, network_request_id, fetch_request_id, client)
437
+ elsif event['resourceType'] && event['resourceType'].to_s.downcase != 'document'
438
+ request_event = build_request_event_from_paused(event, request_id)
439
+ handle_request_from_paused(request_event, fetch_request_id, [], client: client)
440
+ else
441
+ @network_event_manager.store_request_paused(network_request_id, event)
442
+ end
285
443
  end
286
444
  end
287
445
 
@@ -291,7 +449,90 @@ class Puppeteer::NetworkManager
291
449
  request_paused_event['request']['headers'])
292
450
  end
293
451
 
294
- private def handle_request(event, fetch_request_id)
452
+ private def handle_request_will_be_sent_extra_info(event, client)
453
+ network_request_id = event['requestId']
454
+ request = @network_event_manager.get_request(network_request_id)
455
+ if request
456
+ request.update_headers(event['headers'])
457
+ else
458
+ @network_event_manager.request_extra_info(network_request_id) << event
459
+ end
460
+ end
461
+
462
+ private def with_interception_lock
463
+ return yield unless Async::Task.current?
464
+
465
+ @interception_semaphore.acquire do
466
+ yield
467
+ end
468
+ end
469
+
470
+ private def handle_request_without_network_instrumentation(event, client)
471
+ frame = if_present(event['frameId']) { |frame_id| @frame_manager.frame(frame_id) }
472
+ request = Puppeteer::HTTPRequest.new(client, frame, event['requestId'], @user_request_interception_enabled, event, [])
473
+ emit_event(NetworkManagerEmittedEvents::Request, request)
474
+ begin
475
+ with_interception_lock { request.finalize_interceptions }
476
+ rescue => err
477
+ debug_puts(err)
478
+ end
479
+ end
480
+
481
+ private def handle_redirect_request_paused(event, network_request_id, fetch_request_id, client)
482
+ redirect_chain = []
483
+ if_present(@network_event_manager.get_request(network_request_id)) do |request|
484
+ response_payload = build_synthetic_redirect_response(event)
485
+ handle_request_redirect(request, response_payload, nil)
486
+ redirect_chain = request.internal.redirect_chain
487
+ end
488
+
489
+ request_event = build_request_event_from_paused(event, event['networkId'])
490
+ handle_request_from_paused(request_event, fetch_request_id, redirect_chain, client: client)
491
+ end
492
+
493
+ private def build_request_event_from_paused(event, request_id)
494
+ {
495
+ 'requestId' => request_id,
496
+ 'request' => event['request'],
497
+ 'type' => event['resourceType'],
498
+ 'frameId' => event['frameId'],
499
+ 'initiator' => event['initiator'],
500
+ }.compact
501
+ end
502
+
503
+ private def build_synthetic_redirect_response(event)
504
+ status = event['responseStatusCode'] || 302
505
+ headers = {}
506
+ if_present(event.dig('request', 'url')) do |url|
507
+ headers['location'] = url
508
+ end
509
+ {
510
+ 'status' => status,
511
+ 'statusText' => Puppeteer::HTTPRequest::STATUS_TEXTS[status.to_s] || 'Found',
512
+ 'headers' => headers,
513
+ 'fromDiskCache' => false,
514
+ 'fromServiceWorker' => false,
515
+ }
516
+ end
517
+
518
+ private def handle_request_from_paused(event, fetch_request_id, redirect_chain, client:)
519
+ network_request_id = event['requestId']
520
+ frame = if_present(event['frameId']) { |frame_id| @frame_manager.frame(frame_id) }
521
+ request = Puppeteer::HTTPRequest.new(client, frame, fetch_request_id, @user_request_interception_enabled, event, redirect_chain)
522
+ if_present(@network_event_manager.request_extra_info(network_request_id).shift) do |extra_info|
523
+ request.update_headers(extra_info['headers'])
524
+ end
525
+ @network_event_manager.store_request(network_request_id, request)
526
+ emit_event(NetworkManagerEmittedEvents::Request, request)
527
+ begin
528
+ with_interception_lock { request.finalize_interceptions }
529
+ rescue => err
530
+ debug_puts(err)
531
+ end
532
+ end
533
+
534
+ private def handle_request(event, fetch_request_id, from_memory_cache: false, client:)
535
+ network_request_id = event['requestId']
295
536
  redirect_chain = []
296
537
  if event['redirectResponse']
297
538
  # We want to emit a response and requestfinished for the
@@ -303,39 +544,55 @@ class Puppeteer::NetworkManager
303
544
  # response/requestfinished.
304
545
  redirect_response_extra_info = nil
305
546
  if event['redirectHasExtraInfo']
306
- redirect_response_extra_info = @network_event_manager.response_extra_info(event['requestId']).shift
547
+ redirect_response_extra_info = @network_event_manager.response_extra_info(network_request_id).shift
307
548
  unless redirect_response_extra_info
308
549
  redirect_info = RedirectInfo.new(
309
550
  event: event,
310
551
  fetch_request_id: fetch_request_id,
311
552
  )
312
- @network_event_manager.enqueue_redirect_info(event['requestId'], redirect_info)
553
+ @network_event_manager.enqueue_redirect_info(network_request_id, redirect_info)
313
554
  return
314
555
  end
315
556
  end
316
557
 
317
558
  # If we connect late to the target, we could have missed the
318
559
  # requestWillBeSent event.
319
- if_present(@network_event_manager.get_request(event['requestId'])) do |request|
560
+ if_present(@network_event_manager.get_request(network_request_id)) do |request|
320
561
  handle_request_redirect(request, event['redirectResponse'], redirect_response_extra_info)
321
562
  redirect_chain = request.internal.redirect_chain
563
+ if_present(@network_event_manager.request_extra_info(network_request_id).shift) do |extra_info|
564
+ request.update_headers(extra_info['headers'])
565
+ end
322
566
  end
323
567
  end
324
568
  frame = if_present(event['frameId']) { |frame_id| @frame_manager.frame(frame_id) }
325
- request = Puppeteer::HTTPRequest.new(@client, frame, fetch_request_id, @user_request_interception_enabled, event, redirect_chain)
326
- @network_event_manager.store_request(event['requestId'], request)
569
+ request = Puppeteer::HTTPRequest.new(client, frame, fetch_request_id, @user_request_interception_enabled, event, redirect_chain)
570
+ if_present(@network_event_manager.request_extra_info(network_request_id).shift) do |extra_info|
571
+ request.update_headers(extra_info['headers'])
572
+ end
573
+ request.internal.from_memory_cache = from_memory_cache
574
+ @network_event_manager.store_request(network_request_id, request)
327
575
  emit_event(NetworkManagerEmittedEvents::Request, request)
328
576
  begin
329
- request.finalize_interceptions
577
+ with_interception_lock { request.finalize_interceptions }
330
578
  rescue => err
331
579
  debug_puts(err)
332
580
  end
333
581
  end
334
582
 
335
- private def handle_request_served_from_cache(event)
336
- request = @network_event_manager.get_request(event['requestId'])
583
+ private def handle_request_served_from_cache(event, client)
584
+ network_request_id = event['requestId']
585
+ request = @network_event_manager.get_request(network_request_id)
586
+ request_will_be_sent_event = @network_event_manager.get_request_will_be_sent(network_request_id)
337
587
  if request
338
588
  request.internal.from_memory_cache = true
589
+ elsif request_will_be_sent_event
590
+ handle_request(request_will_be_sent_event, nil, from_memory_cache: true, client: client)
591
+ request = @network_event_manager.get_request(network_request_id)
592
+ end
593
+ unless request
594
+ debug_puts("Request #{event['requestId']} was served from cache but we could not find the corresponding request object")
595
+ return
339
596
  end
340
597
  emit_event(NetworkManagerEmittedEvents::RequestServedFromCache, request)
341
598
  end
@@ -343,7 +600,7 @@ class Puppeteer::NetworkManager
343
600
  # @param request [Puppeteer::HTTPRequest]
344
601
  # @param response_payload [Hash]
345
602
  private def handle_request_redirect(request, response_payload, extra_info)
346
- response = Puppeteer::HTTPResponse.new(@client, request, response_payload, extra_info)
603
+ response = Puppeteer::HTTPResponse.new(request.client, request, response_payload, extra_info)
347
604
  request.internal.response = response
348
605
  request.internal.redirect_chain << request
349
606
  response.internal.body_loaded_promise.reject(Puppeteer::HTTPResponse::Redirected.new)
@@ -352,122 +609,146 @@ class Puppeteer::NetworkManager
352
609
  emit_event(NetworkManagerEmittedEvents::RequestFinished, request)
353
610
  end
354
611
 
355
- private def emit_response_event(response_received_event, extra_info)
356
- request = @network_event_manager.get_request(response_received_event['requestId'])
612
+ private def emit_response_event(response_received_event, extra_info, network_request_id:)
613
+ request = @network_event_manager.get_request(network_request_id)
357
614
  # FileUpload sends a response without a matching request.
358
615
  return unless request
359
616
 
360
- unless @network_event_manager.response_extra_info(response_received_event['requestId']).empty?
617
+ unless @network_event_manager.response_extra_info(network_request_id).empty?
361
618
  debug_puts("Unexpected extraInfo events for request #{response_received_event['requestId']}")
362
619
  end
363
620
 
364
- response = Puppeteer::HTTPResponse.new(@client, request, response_received_event['response'], extra_info)
621
+ # Chromium sends wrong extraInfo events for responses served from cache.
622
+ # @see https://crbug.com/1340398
623
+ if response_received_event.dig('response', 'fromDiskCache')
624
+ extra_info = nil
625
+ end
626
+
627
+ response = Puppeteer::HTTPResponse.new(request.client, request, response_received_event['response'], extra_info)
365
628
  request.internal.response = response
366
629
  emit_event(NetworkManagerEmittedEvents::Response, response)
367
630
  end
368
631
 
632
+ private def adopt_cdp_session_if_needed(client, request)
633
+ return if client == request.client
634
+
635
+ request.internal.client = client
636
+ end
637
+
369
638
  # @param event [Hash]
370
- private def handle_response_received(event)
371
- request = @network_event_manager.get_request(event['requestId'])
639
+ private def handle_response_received(event, client)
640
+ network_request_id = event['requestId']
641
+ request = @network_event_manager.get_request(network_request_id)
372
642
  extra_info = nil
373
643
  if request && !request.internal.from_memory_cache? && event['hasExtraInfo']
374
- extra_info = @network_event_manager.response_extra_info(event['requestId']).shift
644
+ extra_info = @network_event_manager.response_extra_info(network_request_id).shift
375
645
 
376
646
  unless extra_info
377
647
  # Wait until we get the corresponding ExtraInfo event.
378
- @network_event_manager.enqueue_event_group(event['requestId'], QueuedEventGroup.new(response_received_event: event))
648
+ @network_event_manager.enqueue_event_group(network_request_id, QueuedEventGroup.new(response_received_event: event))
379
649
  return
380
650
  end
381
651
  end
382
- emit_response_event(event, extra_info)
652
+ emit_response_event(event, extra_info, network_request_id: network_request_id)
383
653
  end
384
654
 
385
- private def handle_response_received_extra_info(event)
655
+ private def handle_response_received_extra_info(event, client)
656
+ network_request_id = event['requestId']
386
657
  # We may have skipped a redirect response/request pair due to waiting for
387
658
  # this ExtraInfo event. If so, continue that work now that we have the
388
659
  # request.
389
- if_present(@network_event_manager.take_queued_redirect_info(event['requestId'])) do |redirect_info|
390
- @network_event_manager.response_extra_info(event['requestId']) << event
391
- handle_request(redirect_info.event, redirect_info)
660
+ if_present(@network_event_manager.take_queued_redirect_info(network_request_id)) do |redirect_info|
661
+ @network_event_manager.response_extra_info(network_request_id) << event
662
+ handle_request(redirect_info.event, redirect_info.fetch_request_id, client: client)
392
663
  return
393
664
  end
394
665
 
395
666
  # We may have skipped response and loading events because we didn't have
396
667
  # this ExtraInfo event yet. If so, emit those events now.
397
- if_present(@network_event_manager.get_queued_event_group(event['requestId'])) do |queued_events|
398
- @network_event_manager.forget_queued_event_group(event['requestId'])
399
- emit_response_event(queued_events.response_received_event, event)
668
+ if_present(@network_event_manager.get_queued_event_group(network_request_id)) do |queued_events|
669
+ @network_event_manager.forget_queued_event_group(network_request_id)
670
+ emit_response_event(queued_events.response_received_event, event, network_request_id: network_request_id)
400
671
  if_present(queued_events.loading_finished_event) do |loading_finished_event|
401
- emit_loading_finished(loading_finished_event)
672
+ emit_loading_finished(loading_finished_event, network_request_id: network_request_id, client: client)
402
673
  end
403
674
  if_present(queued_events.loading_failed_event) do |loading_failed_event|
404
- emit_loading_failed(loading_failed_event)
675
+ emit_loading_failed(loading_failed_event, network_request_id: network_request_id, client: client)
405
676
  end
406
677
  return
407
678
  end
408
679
 
409
680
  # Wait until we get another event that can use this ExtraInfo event.
410
- @network_event_manager.response_extra_info(event['requestId']) << event
681
+ return unless @network_event_manager.get_request(network_request_id)
682
+
683
+ @network_event_manager.response_extra_info(network_request_id) << event
411
684
  end
412
685
 
413
686
  private def forget_request(request, forget_events)
414
687
  request_id = request.internal.request_id
688
+ network_request_id = request_id
415
689
  interception_id = request.internal.interception_id
690
+ auth_request_id = interception_id
416
691
 
417
- @network_event_manager.forget_request(request_id)
418
- @attempted_authentications.delete(interception_id)
692
+ @network_event_manager.forget_request(network_request_id)
693
+ @attempted_authentications.delete(auth_request_id)
419
694
  if forget_events
420
- @network_event_manager.forget(request_id)
695
+ @network_event_manager.forget(network_request_id)
421
696
  end
422
697
  end
423
698
 
424
- private def handle_loading_finished(event)
699
+ private def handle_loading_finished(event, client)
700
+ network_request_id = event['requestId']
425
701
  # If the response event for this request is still waiting on a
426
702
  # corresponding ExtraInfo event, then wait to emit this event too.
427
- queued_events = @network_event_manager.get_queued_event_group(event['requestId'])
703
+ queued_events = @network_event_manager.get_queued_event_group(network_request_id)
428
704
  if queued_events
429
705
  queued_events.loading_finished_event = event
430
706
  else
431
- emit_loading_finished(event)
707
+ emit_loading_finished(event, network_request_id: network_request_id, client: client)
432
708
  end
433
709
  end
434
710
 
435
- private def emit_loading_finished(event)
436
- request = @network_event_manager.get_request(event['requestId'])
711
+ private def emit_loading_finished(event, network_request_id:, client:)
712
+ request = @network_event_manager.get_request(network_request_id)
437
713
  # For certain requestIds we never receive requestWillBeSent event.
438
714
  # @see https://crbug.com/750469
439
715
  return unless request
440
716
 
717
+ adopt_cdp_session_if_needed(client, request)
718
+
441
719
  # Under certain conditions we never get the Network.responseReceived
442
720
  # event from protocol. @see https://crbug.com/883475
443
721
  if_present(request.response) do |response|
444
- response.internal.body_loaded_promise.fulfill(nil)
722
+ response.internal.body_loaded_promise.resolve(nil)
445
723
  end
446
724
 
447
725
  forget_request(request, true)
448
726
  emit_event(NetworkManagerEmittedEvents::RequestFinished, request)
449
727
  end
450
728
 
451
- private def handle_loading_failed(event)
729
+ private def handle_loading_failed(event, client)
730
+ network_request_id = event['requestId']
452
731
  # If the response event for this request is still waiting on a
453
732
  # corresponding ExtraInfo event, then wait to emit this event too.
454
- queued_events = @network_event_manager.get_queued_event_group(event['requestId'])
733
+ queued_events = @network_event_manager.get_queued_event_group(network_request_id)
455
734
  if queued_events
456
735
  queued_events.loading_failed_event = event
457
736
  else
458
- emit_loading_failed(event)
737
+ emit_loading_failed(event, network_request_id: network_request_id, client: client)
459
738
  end
460
739
  end
461
740
 
462
- private def emit_loading_failed(event)
463
- request = @network_event_manager.get_request(event['requestId'])
741
+ private def emit_loading_failed(event, network_request_id:, client:)
742
+ request = @network_event_manager.get_request(network_request_id)
464
743
  # For certain requestIds we never receive requestWillBeSent event.
465
744
  # @see https://crbug.com/750469
466
745
  return unless request
467
746
 
747
+ adopt_cdp_session_if_needed(client, request)
748
+
468
749
  request.internal.failure_text = event['errorText']
469
750
  if_present(request.response) do |response|
470
- response.internal.body_loaded_promise.fulfill(nil)
751
+ response.internal.body_loaded_promise.resolve(nil)
471
752
  end
472
753
  forget_request(request, true)
473
754
  emit_event(NetworkManagerEmittedEvents::RequestFailed, request)