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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4841672d87f3828928fa35646176d878469ade0afce9f9e6a119ccd1cf3251ae
4
- data.tar.gz: 618382810f59ff95713064d91a2ccb6df4c0dd2cf6287f31127cddd5848e8dbd
3
+ metadata.gz: d1d74d19e935fcf05c7bd0893f2113dfac1e6639de926ef8844159118ad13ff5
4
+ data.tar.gz: 3999a5e55251f993b6721d9d3f8c03b8c7588fa0834499a53c808789bb7a3ded
5
5
  SHA512:
6
- metadata.gz: 7abf8800a5943fca86ed9fa19058a1957ed101d52db70fd75042bda91abc2a9070d3bb5e6b0129e18103ebf407de2ea97a5e1a826cec3b5a7c0f21c12d0dfd61
7
- data.tar.gz: 7831958f9d9b2af2db632b341cd8c902b45ed78feaf12dc21ee4f9c80c72237d56acba58e1b2aa7b0c842fc6dd3626beb48fd0e08ef932730920e33c23eb2f4b
6
+ metadata.gz: 1c7bea03d8553b2e8ec76c8216b57e0f07b20157cb1c1710c31b4730281b410c95b61541fae56bc4d3853cedf5e62854e0255b920607e28cfa902e941e779953
7
+ data.tar.gz: 6911fdce7df3238bbe27b28b6a8f747077b12a5cd949b12e4cc0bddadff1433f0ef56662cc361f7d80c90924fe38813ac6584ef0ec08a6b39b0859bc4067d2d4
@@ -2,7 +2,13 @@
2
2
 
3
3
  module Html2rss
4
4
  # The Html2rss::Error base class.
5
- class Error < StandardError; end
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
- @attempts << { strategy:, items_count: nil, error_class: error.class.name }
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(attempts: state.attempts, response: state.last_response)
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(attempts:, response:)
234
- surface_category = surface_category_for(response)
235
- raise NoFeedItemsExtracted.new(attempts:, surface_category:)
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
- raise BlockedSurfaceDetected, "Blocked surface detected: #{error.challenge_message}" if error.challenge_block?
53
-
54
- if error.timeout?
55
- log_timeout!(reason: 'botasaurus_upstream')
56
- raise RequestTimedOut.new(error.failure_message, timeout_phase: error.timeout_phase)
57
- end
58
-
59
- raise BotasaurusServiceError, error.failure_message
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
- monotonic_now + ctx.budget.effective_timeout_seconds(fallback: ctx.policy.total_timeout_seconds)
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.to_sym
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
- def warn_migration_strategy(strategy)
114
- replacement = MIGRATION_REPLACEMENTS.fetch(strategy)
115
- message = "RequestService: strategy ':#{strategy}' is deprecated for migration and will be removed " \
116
- "in a future release. Use ':#{replacement}' instead."
117
- warn(message, category: :deprecated)
118
- Log.warn(message)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Html2rss
4
4
  # Current application version.
5
- VERSION = '0.32.0'
5
+ VERSION = '0.32.1'
6
6
  public_constant :VERSION
7
7
  end
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.0
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.0
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: