html2rss 0.32.0 → 0.32.1
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/lib/html2rss/error.rb +7 -1
- data/lib/html2rss/feed_pipeline/auto_fallback.rb +27 -6
- data/lib/html2rss/request_service/blocked_surface.rb +11 -0
- data/lib/html2rss/request_service/botasaurus_contract.rb +5 -0
- data/lib/html2rss/request_service/botasaurus_strategy.rb +10 -8
- data/lib/html2rss/request_service/httpx_strategy.rb +11 -1
- data/lib/html2rss/request_service.rb +33 -8
- data/lib/html2rss/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1d74d19e935fcf05c7bd0893f2113dfac1e6639de926ef8844159118ad13ff5
|
|
4
|
+
data.tar.gz: 3999a5e55251f993b6721d9d3f8c03b8c7588fa0834499a53c808789bb7a3ded
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c7bea03d8553b2e8ec76c8216b57e0f07b20157cb1c1710c31b4730281b410c95b61541fae56bc4d3853cedf5e62854e0255b920607e28cfa902e941e779953
|
|
7
|
+
data.tar.gz: 6911fdce7df3238bbe27b28b6a8f747077b12a5cd949b12e4cc0bddadff1433f0ef56662cc361f7d80c90924fe38813ac6584ef0ec08a6b39b0859bc4067d2d4
|
data/lib/html2rss/error.rb
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Html2rss
|
|
4
4
|
# The Html2rss::Error base class.
|
|
5
|
-
class Error < StandardError
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
# @return [Array<Hash{Symbol => Object}>, nil] tier attempt diagnostics when set by AutoFallback
|
|
7
|
+
attr_accessor :attempts
|
|
8
|
+
|
|
9
|
+
# @return [Hash{String => Object}, nil] allowlisted upstream transport telemetry when attached
|
|
10
|
+
attr_accessor :transport_meta
|
|
11
|
+
end
|
|
6
12
|
|
|
7
13
|
# Raised when auto fallback exhausts all concrete tiers and extractors find no feed items.
|
|
8
14
|
class NoFeedItemsExtracted < Error
|
|
@@ -30,12 +30,13 @@ module Html2rss
|
|
|
30
30
|
##
|
|
31
31
|
# Mutable run state for one auto-fallback chain: attempts plus selected result.
|
|
32
32
|
class AttemptState
|
|
33
|
-
attr_reader :attempts, :result, :last_response, :entry_resolution
|
|
33
|
+
attr_reader :attempts, :result, :last_response, :last_error, :entry_resolution
|
|
34
34
|
|
|
35
35
|
def initialize
|
|
36
36
|
@attempts = []
|
|
37
37
|
@result = nil
|
|
38
38
|
@last_response = nil
|
|
39
|
+
@last_error = nil
|
|
39
40
|
@entry_resolution = nil
|
|
40
41
|
@resolution_tried = false
|
|
41
42
|
@aborted = false
|
|
@@ -70,7 +71,11 @@ module Html2rss
|
|
|
70
71
|
# @param error [Exception] caught error
|
|
71
72
|
# @return [void]
|
|
72
73
|
def record_error(strategy:, error:)
|
|
73
|
-
|
|
74
|
+
attempt = { strategy:, items_count: nil, error_class: error.class.name }
|
|
75
|
+
meta = error.is_a?(Html2rss::Error) ? error.transport_meta : nil
|
|
76
|
+
attempt[:transport_meta] = meta if meta&.any?
|
|
77
|
+
@attempts << attempt
|
|
78
|
+
@last_error = error
|
|
74
79
|
end
|
|
75
80
|
|
|
76
81
|
# @param strategy [Symbol] strategy that returned a response
|
|
@@ -78,6 +83,7 @@ module Html2rss
|
|
|
78
83
|
# @param transport_meta [Hash, nil] optional allowlisted upstream telemetry
|
|
79
84
|
# @return [void]
|
|
80
85
|
def record_items(strategy:, items_count:, transport_meta: nil)
|
|
86
|
+
@last_error = nil
|
|
81
87
|
attempt = { strategy:, items_count:, error_class: nil }
|
|
82
88
|
attempt[:transport_meta] = transport_meta if transport_meta && !transport_meta.empty?
|
|
83
89
|
@attempts << attempt
|
|
@@ -136,7 +142,7 @@ module Html2rss
|
|
|
136
142
|
state = run_attempts
|
|
137
143
|
return state.result if state.succeeded?
|
|
138
144
|
|
|
139
|
-
finalize_failure(
|
|
145
|
+
finalize_failure(state)
|
|
140
146
|
end
|
|
141
147
|
|
|
142
148
|
private
|
|
@@ -230,9 +236,24 @@ module Html2rss
|
|
|
230
236
|
"budget_remaining=#{budget_remaining_label}")
|
|
231
237
|
end
|
|
232
238
|
|
|
233
|
-
def finalize_failure(
|
|
234
|
-
|
|
235
|
-
|
|
239
|
+
def finalize_failure(state)
|
|
240
|
+
if raise_last_error?(state)
|
|
241
|
+
error = state.last_error
|
|
242
|
+
error.attempts = state.attempts if error.is_a?(Html2rss::Error)
|
|
243
|
+
raise error
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
raise NoFeedItemsExtracted.new(
|
|
247
|
+
attempts: state.attempts,
|
|
248
|
+
surface_category: surface_category_for(state.last_response)
|
|
249
|
+
)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def raise_last_error?(state)
|
|
253
|
+
error = state.last_error
|
|
254
|
+
return false if error.nil? || error.is_a?(RequestService::BotasaurusConfigurationError)
|
|
255
|
+
|
|
256
|
+
true
|
|
236
257
|
end
|
|
237
258
|
|
|
238
259
|
def surface_category_for(response)
|
|
@@ -47,6 +47,17 @@ module Html2rss
|
|
|
47
47
|
message: 'Blocked surface detected: Vercel Security Checkpoint. ' \
|
|
48
48
|
'This site is a JS-rendered SPA behind Vercel edge protection. ' \
|
|
49
49
|
'Configure BOTASAURUS_SCRAPER_URL or target a direct listing URL.'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: :http_403_forbidden,
|
|
53
|
+
min_matches: 2,
|
|
54
|
+
patterns: [
|
|
55
|
+
%r{<title>\s*403\s+forbidden\s*</title>}i,
|
|
56
|
+
%r{<h1>\s*403\s+forbidden\s*</h1>}i
|
|
57
|
+
],
|
|
58
|
+
message: 'Blocked surface detected: HTTP 403 Forbidden access block page. ' \
|
|
59
|
+
'Target a direct listing URL, configure BOTASAURUS_SCRAPER_URL, ' \
|
|
60
|
+
'or run from an environment that is not IP-blocked.'
|
|
50
61
|
}
|
|
51
62
|
].freeze
|
|
52
63
|
|
|
@@ -198,6 +198,11 @@ module Html2rss
|
|
|
198
198
|
"Botasaurus scrape failed (#{details.join(', ')})."
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
+
# @return [Hash{String => Object}] allowlisted diagnostics plus error_category (frozen)
|
|
202
|
+
def transport_meta
|
|
203
|
+
diagnostics.merge('error_category' => error_category).freeze
|
|
204
|
+
end
|
|
205
|
+
|
|
201
206
|
private
|
|
202
207
|
|
|
203
208
|
attr_reader :payload, :transport_status
|
|
@@ -49,14 +49,16 @@ module Html2rss
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def raise_from_error!(error)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
err = if error.challenge_block?
|
|
53
|
+
BlockedSurfaceDetected.new("Blocked surface detected: #{error.challenge_message}")
|
|
54
|
+
elsif error.timeout?
|
|
55
|
+
log_timeout!(reason: 'botasaurus_upstream')
|
|
56
|
+
RequestTimedOut.new(error.failure_message, timeout_phase: error.timeout_phase)
|
|
57
|
+
else
|
|
58
|
+
BotasaurusServiceError.new(error.failure_message)
|
|
59
|
+
end
|
|
60
|
+
err.transport_meta = error.transport_meta
|
|
61
|
+
raise err
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
def response_url(final_url)
|
|
@@ -60,8 +60,18 @@ module Html2rss
|
|
|
60
60
|
|
|
61
61
|
private
|
|
62
62
|
|
|
63
|
+
# Caps one HTTPX execute (including internal MAX_RETRIES) so a hanging
|
|
64
|
+
# :default tier cannot consume the full shared auto-fallback budget.
|
|
65
|
+
#
|
|
66
|
+
# @return [Float] monotonic deadline
|
|
63
67
|
def request_deadline
|
|
64
|
-
|
|
68
|
+
budget_timeout = ctx.budget.effective_timeout_seconds(fallback: ctx.policy.total_timeout_seconds)
|
|
69
|
+
monotonic_now + [budget_timeout, attempt_timeout_ceiling].min
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [Float] connect + read ceiling for a single attempt
|
|
73
|
+
def attempt_timeout_ceiling
|
|
74
|
+
(ctx.policy.connect_timeout_seconds + ctx.policy.read_timeout_seconds).to_f
|
|
65
75
|
end
|
|
66
76
|
|
|
67
77
|
def reset_redirect_tracking!
|
|
@@ -98,24 +98,49 @@ module Html2rss
|
|
|
98
98
|
# @return [Response] the response from the executed strategy.
|
|
99
99
|
# @raise [UnknownStrategy] if the strategy is not registered.
|
|
100
100
|
def execute(ctx, strategy: default_strategy_name)
|
|
101
|
-
strategy_sym = strategy.
|
|
101
|
+
strategy_sym = resolve_strategy_name(strategy, relation: ctx.relation, host: ctx.url.host)
|
|
102
102
|
strategy_class = STRATEGIES.fetch(strategy_sym) do
|
|
103
103
|
raise UnknownStrategy,
|
|
104
104
|
"The strategy '#{strategy}' is not known. Available strategies: #{strategy_names.join(', ')}"
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
warn_migration_strategy(strategy_sym) if MIGRATION_REPLACEMENTS.key?(strategy_sym)
|
|
108
107
|
strategy_class.new(ctx).execute
|
|
109
108
|
end
|
|
110
109
|
|
|
111
110
|
private
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
##
|
|
113
|
+
# Resolves deprecated migration aliases to their replacement strategy name.
|
|
114
|
+
# @param strategy [Symbol, String] requested strategy name
|
|
115
|
+
# @param relation [Symbol] request relation (+ :initial + or follow-up)
|
|
116
|
+
# @param host [String, nil] request host for structured deprecation telemetry
|
|
117
|
+
# @return [Symbol] canonical strategy name after migration remapping
|
|
118
|
+
def resolve_strategy_name(strategy, relation:, host:)
|
|
119
|
+
strategy_sym = strategy.to_sym
|
|
120
|
+
replacement = MIGRATION_REPLACEMENTS[strategy_sym]
|
|
121
|
+
return strategy_sym unless replacement
|
|
122
|
+
|
|
123
|
+
warn_migration_strategy(strategy_sym, replacement, relation:, host:)
|
|
124
|
+
replacement
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Emits a structured +strategy_deprecated+ warning once per session (+ :initial + only).
|
|
129
|
+
# @param strategy [Symbol] deprecated strategy alias
|
|
130
|
+
# @param replacement [Symbol] preferred strategy name
|
|
131
|
+
# @param relation [Symbol] request relation
|
|
132
|
+
# @param host [String, nil] request host
|
|
133
|
+
# @return [void]
|
|
134
|
+
def warn_migration_strategy(strategy, replacement, relation:, host:)
|
|
135
|
+
return unless relation == :initial
|
|
136
|
+
|
|
137
|
+
Log.warn({
|
|
138
|
+
event_name: 'strategy_deprecated',
|
|
139
|
+
strategy: strategy.to_s,
|
|
140
|
+
replacement: replacement.to_s,
|
|
141
|
+
host:,
|
|
142
|
+
message: "#{name}: strategy=#{strategy.inspect} is deprecated; using #{replacement.inspect} instead."
|
|
143
|
+
}.to_json)
|
|
119
144
|
end
|
|
120
145
|
end
|
|
121
146
|
end
|
data/lib/html2rss/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: html2rss
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.32.
|
|
4
|
+
version: 0.32.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gil Desmarais
|
|
@@ -478,7 +478,7 @@ licenses:
|
|
|
478
478
|
- MIT
|
|
479
479
|
metadata:
|
|
480
480
|
allowed_push_host: https://rubygems.org
|
|
481
|
-
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.32.
|
|
481
|
+
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.32.1
|
|
482
482
|
rubygems_mfa_required: 'true'
|
|
483
483
|
rdoc_options: []
|
|
484
484
|
require_paths:
|