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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -3
- data/AGENTS.md +169 -0
- data/CLAUDE/README.md +41 -0
- data/CLAUDE/architecture.md +253 -0
- data/CLAUDE/cdp_protocol.md +230 -0
- data/CLAUDE/concurrency.md +216 -0
- data/CLAUDE/porting_puppeteer.md +575 -0
- data/CLAUDE/rbs_type_checking.md +101 -0
- data/CLAUDE/spec_migration_plans.md +1041 -0
- data/CLAUDE/testing.md +278 -0
- data/CLAUDE.md +242 -0
- data/README.md +8 -0
- data/Rakefile +7 -0
- data/Steepfile +28 -0
- data/docs/api_coverage.md +105 -56
- data/lib/puppeteer/aria_query_handler.rb +3 -2
- data/lib/puppeteer/async_utils.rb +214 -0
- data/lib/puppeteer/browser.rb +98 -56
- data/lib/puppeteer/browser_connector.rb +18 -3
- data/lib/puppeteer/browser_context.rb +196 -3
- data/lib/puppeteer/browser_runner.rb +18 -10
- data/lib/puppeteer/cdp_session.rb +67 -23
- data/lib/puppeteer/chrome_target_manager.rb +65 -40
- data/lib/puppeteer/connection.rb +55 -36
- data/lib/puppeteer/console_message.rb +9 -1
- data/lib/puppeteer/console_patch.rb +47 -0
- data/lib/puppeteer/css_coverage.rb +5 -3
- data/lib/puppeteer/custom_query_handler.rb +80 -33
- data/lib/puppeteer/define_async_method.rb +31 -37
- data/lib/puppeteer/dialog.rb +47 -14
- data/lib/puppeteer/element_handle.rb +231 -62
- data/lib/puppeteer/emulation_manager.rb +1 -1
- data/lib/puppeteer/env.rb +1 -1
- data/lib/puppeteer/errors.rb +25 -2
- data/lib/puppeteer/event_callbackable.rb +15 -0
- data/lib/puppeteer/events.rb +4 -0
- data/lib/puppeteer/execution_context.rb +148 -3
- data/lib/puppeteer/file_chooser.rb +6 -0
- data/lib/puppeteer/frame.rb +162 -91
- data/lib/puppeteer/frame_manager.rb +69 -48
- data/lib/puppeteer/http_request.rb +114 -38
- data/lib/puppeteer/http_response.rb +24 -7
- data/lib/puppeteer/isolated_world.rb +64 -41
- data/lib/puppeteer/js_coverage.rb +5 -3
- data/lib/puppeteer/js_handle.rb +58 -16
- data/lib/puppeteer/keyboard.rb +30 -17
- data/lib/puppeteer/launcher/browser_options.rb +3 -1
- data/lib/puppeteer/launcher/chrome.rb +8 -5
- data/lib/puppeteer/launcher/launch_options.rb +7 -2
- data/lib/puppeteer/launcher.rb +4 -8
- data/lib/puppeteer/lifecycle_watcher.rb +38 -22
- data/lib/puppeteer/mouse.rb +273 -64
- data/lib/puppeteer/network_event_manager.rb +7 -0
- data/lib/puppeteer/network_manager.rb +393 -112
- data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
- data/lib/puppeteer/page.rb +568 -226
- data/lib/puppeteer/puppeteer.rb +171 -64
- data/lib/puppeteer/query_handler_manager.rb +112 -16
- data/lib/puppeteer/reactor_runner.rb +247 -0
- data/lib/puppeteer/remote_object.rb +127 -47
- data/lib/puppeteer/target.rb +74 -27
- data/lib/puppeteer/task_manager.rb +3 -1
- data/lib/puppeteer/timeout_helper.rb +6 -10
- data/lib/puppeteer/touch_handle.rb +39 -0
- data/lib/puppeteer/touch_screen.rb +72 -22
- data/lib/puppeteer/tracing.rb +3 -3
- data/lib/puppeteer/version.rb +1 -1
- data/lib/puppeteer/wait_task.rb +264 -101
- data/lib/puppeteer/web_socket.rb +2 -2
- data/lib/puppeteer/web_socket_transport.rb +91 -27
- data/lib/puppeteer/web_worker.rb +175 -0
- data/lib/puppeteer.rb +20 -4
- data/puppeteer-ruby.gemspec +15 -11
- data/sig/_external.rbs +8 -0
- data/sig/_supplementary.rbs +314 -0
- data/sig/puppeteer/browser.rbs +166 -0
- data/sig/puppeteer/cdp_session.rbs +64 -0
- data/sig/puppeteer/dialog.rbs +41 -0
- data/sig/puppeteer/element_handle.rbs +305 -0
- data/sig/puppeteer/execution_context.rbs +87 -0
- data/sig/puppeteer/frame.rbs +226 -0
- data/sig/puppeteer/http_request.rbs +214 -0
- data/sig/puppeteer/http_response.rbs +89 -0
- data/sig/puppeteer/js_handle.rbs +64 -0
- data/sig/puppeteer/keyboard.rbs +40 -0
- data/sig/puppeteer/mouse.rbs +113 -0
- data/sig/puppeteer/page.rbs +515 -0
- data/sig/puppeteer/puppeteer.rbs +98 -0
- data/sig/puppeteer/remote_object.rbs +78 -0
- data/sig/puppeteer/touch_handle.rbs +21 -0
- data/sig/puppeteer/touch_screen.rbs +35 -0
- data/sig/puppeteer/web_worker.rbs +83 -0
- metadata +116 -45
- data/CHANGELOG.md +0 -397
- data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
- data/lib/puppeteer/firefox_target_manager.rb +0 -157
- 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
|
-
|
|
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] ||=
|
|
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 =
|
|
85
|
+
results = Puppeteer::AsyncUtils.await_promise_all(*promises)
|
|
87
86
|
frame_tree = results[1]['frameTree']
|
|
88
87
|
handle_frame_tree(client, frame_tree)
|
|
89
|
-
|
|
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)&.
|
|
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 <
|
|
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
|
-
|
|
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 =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
141
|
+
Puppeteer::AsyncUtils.await_promise_race(
|
|
138
142
|
navigate,
|
|
139
143
|
watcher.timeout_or_termination_promise,
|
|
140
144
|
)
|
|
141
145
|
|
|
142
|
-
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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] ||=
|
|
286
|
-
|
|
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)&.
|
|
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 <
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
475
|
+
if key_session_id != session_id
|
|
459
476
|
true # keep
|
|
460
477
|
else
|
|
461
|
-
|
|
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
|
-
|
|
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
|
|
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, :
|
|
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
|
|
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(
|
|
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.
|
|
222
|
-
|
|
223
|
-
|
|
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:
|
|
271
|
+
{ name: name, value: value.to_s }
|
|
227
272
|
end
|
|
228
273
|
end
|
|
229
274
|
end
|
|
230
275
|
|
|
231
|
-
class InterceptionNotEnabledError <
|
|
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 <
|
|
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
|
-
|
|
268
|
-
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 ==
|
|
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
|
-
|
|
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 ==
|
|
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?(
|
|
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
|
|
369
|
-
mock_response_headers['content-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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 = {
|