html2rss 0.29.1 → 0.30.0

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.
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httpx'
4
+ require 'httpx/plugins/follow_redirects'
5
+ require 'httpx/plugins/callbacks'
6
+ require 'httpx/plugins/ssrf_filter'
7
+ require 'httpx/plugins/brotli'
8
+ require 'httpx/plugins/fiber_concurrency'
9
+ require 'httpx/plugins/retries'
10
+
11
+ module Html2rss
12
+ class RequestService
13
+ ##
14
+ # Strategy to use HTTPX for HTTP requests.
15
+ # Provides native HTTP/2 with ALPN, built-in SSRF protection, streaming byte limits,
16
+ # and redirect handling without monkey-patching.
17
+ # rubocop:disable-next Metrics/ClassLength -- terminal redirect retry colocated with HTTPX transport
18
+ class HttpxStrategy < Strategy
19
+ class << self
20
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
21
+ # @return [HTTPX::Session]
22
+ def base_session
23
+ @base_sessions ||= {}
24
+ @base_sessions[HTTPX::Session] ||= HTTPX
25
+ .plugin(:follow_redirects)
26
+ .plugin(:callbacks)
27
+ .plugin(:brotli)
28
+ .plugin(:fiber_concurrency)
29
+ .plugin(:retries)
30
+ end
31
+
32
+ # @return [HTTPX::Session]
33
+ def base_ssrf_session
34
+ @base_ssrf_sessions ||= {}
35
+ @base_ssrf_sessions[HTTPX::Session] ||= base_session.plugin(:ssrf_filter)
36
+ end
37
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
38
+ end
39
+
40
+ ##
41
+ # @return [ResponseGuard]
42
+ attr_reader :response_guard
43
+
44
+ # Executes the request with runtime policy enforcement, returning the normalized response.
45
+ #
46
+ # @return [Response] normalized response
47
+ def perform_execute
48
+ deadline = request_deadline
49
+ @response_guard = ResponseGuard.new(policy: ctx.policy)
50
+ reset_redirect_tracking!
51
+ raw_response = request_with_terminal_redirect_retry(response_guard, deadline:)
52
+ build_response(raw_response)
53
+ end
54
+
55
+ private
56
+
57
+ def request_deadline
58
+ monotonic_now + ctx.budget.effective_timeout_seconds(fallback: ctx.policy.total_timeout_seconds)
59
+ end
60
+
61
+ def reset_redirect_tracking!
62
+ @last_redirect_to = nil
63
+ @terminal_redirect_retried = false
64
+ @request_url_override = nil
65
+ @current_url = request_url
66
+ end
67
+
68
+ def request_with_terminal_redirect_retry(response_guard, deadline:)
69
+ raw_response = execute_http_request(response_guard, deadline:)
70
+ return raw_response unless redirect_limit_reached?(raw_response)
71
+
72
+ unless terminal_redirect_retryable?(raw_response)
73
+ raise RedirectLimitReached, "Too many redirects (status #{raw_response.status})"
74
+ end
75
+
76
+ retry_from_terminal_redirect!(raw_response, response_guard, deadline:)
77
+ end
78
+
79
+ def redirect_limit_reached?(response)
80
+ return false unless response.is_a?(HTTPX::Response)
81
+ return false if response.status == 304
82
+ return false unless (300..399).cover?(response.status)
83
+
84
+ location = response.headers['location']
85
+ !location.nil? && !location.strip.empty?
86
+ end
87
+
88
+ def terminal_redirect_url(raw_response)
89
+ location = raw_response.headers['location']
90
+ if location && !location.empty?
91
+ base = raw_response.uri || request_url
92
+ return normalize_url(URI.join(base.to_s, location))
93
+ end
94
+
95
+ @last_redirect_to
96
+ end
97
+
98
+ def terminal_redirect_retryable?(raw_response)
99
+ return false if @terminal_redirect_retried
100
+
101
+ target = terminal_redirect_url(raw_response)
102
+ target && target.to_s != request_url.to_s
103
+ end
104
+
105
+ def retry_from_terminal_redirect!(raw_response, response_guard, deadline:)
106
+ terminal_url = terminal_redirect_url(raw_response)
107
+ @terminal_redirect_retried = true
108
+ Log.debug("#{self.class}: redirect limit reached; retrying once from #{terminal_url}")
109
+ begin_terminal_url_request!(terminal_url)
110
+ new_response = execute_http_request(response_guard, deadline:, consume_budget: false)
111
+ if redirect_limit_reached?(new_response)
112
+ raise RedirectLimitReached, "Too many redirects (status #{new_response.status})"
113
+ end
114
+
115
+ new_response
116
+ end
117
+
118
+ def begin_terminal_url_request!(terminal_url)
119
+ ctx.policy.validate_request!(url: terminal_url, origin_url: ctx.origin_url, relation: ctx.relation)
120
+ @request_url_override = terminal_url
121
+ @last_redirect_to = nil
122
+ @current_url = terminal_url
123
+ end
124
+
125
+ def request_url
126
+ @request_url_override || ctx.url
127
+ end
128
+
129
+ def execute_http_request(response_guard, deadline:, consume_budget: true)
130
+ preflight!(consume_budget:)
131
+ session = build_session(response_guard, deadline:)
132
+ response = session.get(request_url.to_s, headers: ctx.headers)
133
+ raise response.error if response.is_a?(HTTPX::ErrorResponse)
134
+
135
+ response
136
+ end
137
+
138
+ def build_session(_response_guard, deadline:)
139
+ session_client(deadline)
140
+ end
141
+
142
+ def session_client(deadline)
143
+ session = ctx.policy.allow_private_networks? ? self.class.base_session : self.class.base_ssrf_session
144
+ session.with(session_options(deadline))
145
+ end
146
+
147
+ def session_options(deadline)
148
+ remaining = remaining_timeout_seconds(deadline)
149
+ {
150
+ timeout: session_timeouts(remaining),
151
+ max_redirects: ctx.policy.max_redirects,
152
+ follow_insecure_redirects: false,
153
+ max_response_body_size: ctx.policy.max_response_bytes,
154
+ resolver_class: :system,
155
+ redirect_on: redirect_callback
156
+ }
157
+ end
158
+
159
+ def session_timeouts(remaining)
160
+ {
161
+ connect_timeout: [ctx.policy.connect_timeout_seconds, remaining].min,
162
+ read_timeout: [ctx.policy.read_timeout_seconds, remaining].min,
163
+ operation_timeout: remaining,
164
+ request_timeout: remaining,
165
+ total_request_timeout: remaining
166
+ }
167
+ end
168
+
169
+ def redirect_callback
170
+ lambda do |redirect_uri|
171
+ to_url = normalize_url(redirect_uri)
172
+ from_url = @current_url
173
+ @last_redirect_to = to_url
174
+ ctx.policy.validate_redirect!(from_url:, to_url:, origin_url: ctx.origin_url, relation: ctx.relation)
175
+ @current_url = to_url
176
+ true
177
+ end
178
+ end
179
+
180
+ def build_response(response)
181
+ headers = response.headers.to_h
182
+ Response.new(
183
+ body: CompressedBody.decode(response.body.to_s, headers:),
184
+ headers:,
185
+ url: response_url(response),
186
+ status: response.status
187
+ )
188
+ end
189
+
190
+ def response_url(response)
191
+ return ctx.url unless (uri = response.uri)
192
+
193
+ Html2rss::Url.from_absolute(uri.to_s)
194
+ end
195
+
196
+ def normalize_url(url)
197
+ Html2rss::Url.from_absolute(url.to_s)
198
+ end
199
+
200
+ def remaining_timeout_seconds(deadline)
201
+ remaining = deadline - monotonic_now
202
+ raise RequestTimedOut, 'Request timed out' if remaining <= 0
203
+
204
+ remaining
205
+ end
206
+
207
+ def monotonic_now
208
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
209
+ end
210
+
211
+ def timeout_error?(error)
212
+ error.is_a?(HTTPX::TimeoutError) || super
213
+ end
214
+
215
+ def ssrf_error?(error)
216
+ error.is_a?(HTTPX::ServerSideRequestForgeryError) || super
217
+ end
218
+
219
+ def response_too_large_error?(error)
220
+ (error.is_a?(HTTPX::Error) && error.message.include?('maximum response body size exceeded')) || super
221
+ end
222
+
223
+ def connection_error?(error)
224
+ error.is_a?(HTTPX::ConnectionError) || error.is_a?(HTTPX::TLSError) || super
225
+ end
226
+ end
227
+ end
228
+ end
@@ -9,22 +9,6 @@ module Html2rss
9
9
  # @param policy [Policy] request policy that defines byte ceilings
10
10
  def initialize(policy:)
11
11
  @policy = policy
12
- @streamed_bytes = 0
13
- end
14
-
15
- ##
16
- # Validates response headers and streamed byte count.
17
- #
18
- # @param total_bytes [Integer] cumulative byte count received so far
19
- # @param headers [Hash, nil] response headers if known
20
- # @return [void]
21
- # @raise [ResponseTooLarge] if the response exceeds configured limits
22
- def inspect_chunk!(total_bytes:, headers: nil)
23
- header_length = headers&.fetch('content-length', headers&.fetch('Content-Length', nil))
24
- raise_if_too_large!(header_length.to_i, policy.max_response_bytes) if header_length
25
-
26
- @streamed_bytes = total_bytes
27
- raise_if_too_large!(@streamed_bytes, policy.max_response_bytes)
28
12
  end
29
13
 
30
14
  ##
@@ -97,11 +97,16 @@ module Html2rss
97
97
  # @param error [StandardError]
98
98
  # @return [void]
99
99
  # @raise [StandardError]
100
+ # rubocop:disable-next Metrics/AbcSize, Metrics/MethodLength -- error translation dispatch
100
101
  def handle_error(error)
101
102
  if timeout_error?(error)
102
103
  log_timeout!(reason: 'transport')
103
104
  Log.debug("#{self.class}: transport timeout message=#{error.message}")
104
105
  raise RequestTimedOut, error.message
106
+ elsif ssrf_error?(error)
107
+ raise PrivateNetworkDenied, error.message
108
+ elsif response_too_large_error?(error)
109
+ raise ResponseTooLarge, "Response exceeded #{ctx.policy.max_response_bytes} bytes"
105
110
  elsif connection_error?(error)
106
111
  translate_connection_error(error)
107
112
  else
@@ -135,14 +140,25 @@ module Html2rss
135
140
  # @param error [StandardError]
136
141
  # @return [Boolean]
137
142
  def timeout_error?(error)
138
- error.is_a?(Faraday::TimeoutError) ||
139
- error.is_a?(Timeout::Error)
143
+ error.is_a?(Timeout::Error)
144
+ end
145
+
146
+ # @param _error [StandardError]
147
+ # @return [Boolean]
148
+ def ssrf_error?(_error)
149
+ false
150
+ end
151
+
152
+ # @param _error [StandardError]
153
+ # @return [Boolean]
154
+ def response_too_large_error?(_error)
155
+ false
140
156
  end
141
157
 
142
158
  # @param error [StandardError]
143
159
  # @return [Boolean]
144
160
  def connection_error?(error)
145
- error.is_a?(Faraday::ConnectionFailed) || error.is_a?(Faraday::SSLError)
161
+ error.is_a?(SocketError) || error.is_a?(SystemCallError)
146
162
  end
147
163
  end
148
164
  end
@@ -1,18 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
4
- require 'forwardable'
5
-
6
3
  module Html2rss
7
4
  ##
8
5
  # Requests website URLs to retrieve their HTML for further processing.
9
- # Provides concrete transport strategies (e.g. Faraday, Botasaurus).
6
+ # Provides concrete transport strategies (e.g. HttpxStrategy, BotasaurusStrategy).
10
7
  #
11
8
  # Feed-level +:auto+ is not registered here — {FeedPipeline::StrategyPlan} resolves
12
9
  # it to a concrete strategy (or {FeedPipeline::AutoFallback} chain) before execute.
13
10
  class RequestService
14
- include Singleton
15
-
16
11
  # Raised when an unknown request strategy is requested.
17
12
  class UnknownStrategy < Html2rss::Error; end
18
13
  # Raised when a URL cannot be parsed or validated.
@@ -59,94 +54,60 @@ module Html2rss
59
54
  # Raised when Botasaurus responds but the scrape fails (upstream error, bad payload).
60
55
  class BotasaurusServiceError < Html2rss::Error; end
61
56
 
62
- class << self
63
- extend Forwardable
64
-
65
- %i[default_strategy_name
66
- default_strategy_name=
67
- strategy_names
68
- register_strategy
69
- unregister_strategy
70
- strategy_registered?
71
- execute].each do |method|
72
- def_delegator :instance, method
73
- end
74
- end
75
-
76
- def initialize
77
- @strategies = {
78
- faraday: FaradayStrategy,
79
- botasaurus: BotasaurusStrategy,
80
- local_file: LocalFileStrategy
81
- }
82
- @default_strategy_name = :faraday
83
- end
57
+ # Map of supported strategy names to their implementation classes.
58
+ # @return [Hash{Symbol => Class<Strategy>}]
59
+ STRATEGIES = {
60
+ default: HttpxStrategy,
61
+ httpx: HttpxStrategy,
62
+ faraday: HttpxStrategy,
63
+ botasaurus: BotasaurusStrategy,
64
+ local_file: LocalFileStrategy
65
+ }.freeze
84
66
 
85
- # @return [Symbol] the default strategy name
86
- attr_reader :default_strategy_name
67
+ # Canonical default strategy symbol.
68
+ # @return [Symbol]
69
+ DEFAULT_STRATEGY_NAME = :default
87
70
 
88
- ##
89
- # Sets the default strategy.
90
- # @param strategy [Symbol] the name of the strategy
91
- # @return [Symbol] the selected default strategy name
92
- # @raise [UnknownStrategy] if the strategy is not registered
93
- def default_strategy_name=(strategy)
94
- raise UnknownStrategy unless strategy_registered?(strategy)
95
-
96
- @default_strategy_name = strategy.to_sym
97
- end
71
+ class << self
72
+ # @return [Symbol] the default strategy name
73
+ def default_strategy_name = DEFAULT_STRATEGY_NAME
98
74
 
99
- # @return [Array<String>] the names of the registered strategies
100
- def strategy_names = @strategies.keys.map(&:to_s)
75
+ # @return [Array<String>] the names of the registered strategies
76
+ def strategy_names = STRATEGIES.keys.map(&:to_s)
101
77
 
102
- ##
103
- # Registers a new strategy.
104
- # @param name [Symbol] the name of the strategy
105
- # @param strategy_class [Class] the class implementing the strategy
106
- # @return [Class] the registered strategy class
107
- # @raise [ArgumentError] if strategy_class is not a Class
108
- def register_strategy(name, strategy_class)
109
- unless strategy_class.is_a?(Class)
110
- raise ArgumentError, "Expected a Class for strategy, got #{strategy_class.class}"
78
+ ##
79
+ # Checks if a strategy is registered.
80
+ # @param name [Symbol, String] the name of the strategy
81
+ # @return [Boolean] true if the strategy is registered, false otherwise.
82
+ def strategy_registered?(name)
83
+ STRATEGIES.key?(name.to_sym)
111
84
  end
112
85
 
113
- @strategies[name.to_sym] = strategy_class
114
- end
115
-
116
- ##
117
- # Checks if a strategy is registered.
118
- # @param name [Symbol] the name of the strategy
119
- # @return [Boolean] true if the strategy is registered, false otherwise.
120
- def strategy_registered?(name)
121
- @strategies.key?(name.to_sym)
122
- end
123
-
124
- ##
125
- # Unregisters a strategy.
126
- # @param name [Symbol] the name of the strategy
127
- # @return [Boolean] true if the strategy was unregistered, false otherwise.
128
- # @raise [ArgumentError] if attempting to unregister the default strategy.
129
- def unregister_strategy(name) # rubocop:disable Naming/PredicateMethod
130
- name_sym = name.to_sym
131
- raise ArgumentError, 'Cannot unregister the default strategy.' if name_sym == @default_strategy_name
86
+ ##
87
+ # Executes the request using the specified strategy.
88
+ # @param ctx [Context] the context for the request.
89
+ # @param strategy [Symbol, String] the strategy to use (defaults to the default strategy).
90
+ # @return [Response] the response from the executed strategy.
91
+ # @raise [UnknownStrategy] if the strategy is not registered.
92
+ def execute(ctx, strategy: default_strategy_name)
93
+ strategy_sym = strategy.to_sym
94
+ strategy_class = STRATEGIES.fetch(strategy_sym) do
95
+ raise UnknownStrategy,
96
+ "The strategy '#{strategy}' is not known. Available strategies: #{strategy_names.join(', ')}"
97
+ end
98
+
99
+ warn_migration_strategy(strategy_sym) if strategy_sym == :faraday
100
+ strategy_class.new(ctx).execute
101
+ end
132
102
 
133
- !!@strategies.delete(name_sym)
134
- end
103
+ private
135
104
 
136
- ##
137
- # Executes the request using the specified strategy.
138
- # @param ctx [Context] the context for the request.
139
- # @param strategy [Symbol] the strategy to use (defaults to the default strategy).
140
- # @return [Response] the response from the executed strategy.
141
- # @raise [ArgumentError] if the context is nil.
142
- # @raise [UnknownStrategy] if the strategy is not registered.
143
- def execute(ctx, strategy: default_strategy_name)
144
- strategy_class = @strategies.fetch(strategy.to_sym) do
145
- raise UnknownStrategy,
146
- "The strategy '#{strategy}' is not known. Available strategies: #{strategy_names.join(', ')}"
105
+ def warn_migration_strategy(strategy)
106
+ message = "RequestService: strategy ':#{strategy}' is deprecated for migration and will be removed " \
107
+ "in a future release. Use ':default' instead."
108
+ warn(message, category: :deprecated)
109
+ Log.warn(message)
147
110
  end
148
-
149
- strategy_class.new(ctx).execute
150
111
  end
151
112
  end
152
113
  end
data/lib/html2rss/test.rb CHANGED
@@ -197,7 +197,8 @@ module Html2rss
197
197
 
198
198
  channel_title = feed_result.channel_title
199
199
  channel_url = raw_config.dig(:channel, :url).to_s
200
- strategy_used = feed_result.status.selected_strategy || raw_config[:strategy] || :faraday
200
+ strategy_used = feed_result.status.selected_strategy || raw_config[:strategy] ||
201
+ RequestService.default_strategy_name
201
202
  min_items_passed = item_count >= min_items
202
203
  quality_failed = strict_quality && min_items_passed && quality_failure?(quality_report)
203
204
  passed = min_items_passed && !quality_failed
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Html2rss
4
4
  # Current application version.
5
- VERSION = '0.29.1'
5
+ VERSION = '0.30.0'
6
6
  public_constant :VERSION
7
7
  end
data/lib/html2rss.rb CHANGED
@@ -33,7 +33,7 @@ module Html2rss # rubocop:disable Metrics/ModuleLength
33
33
  # Golden path step 1 (optional); use {.recon} for verdict and native_feed.
34
34
  #
35
35
  # @param url [String] source page URL
36
- # @param strategy [Symbol] request strategy (:auto, :faraday, :botasaurus)
36
+ # @param strategy [Symbol] request strategy (:auto, :default, :botasaurus)
37
37
  # @param deep [Boolean] when true and strategy is :auto, one Botasaurus hop if configured
38
38
  # @return [Html2rss::PageRecon::Diagnostics::Report]
39
39
  def self.inspect(url, strategy: :auto, deep: false, **)
@@ -45,7 +45,7 @@ module Html2rss # rubocop:disable Metrics/ModuleLength
45
45
  # Golden path step 2 (optional); adds verdict beyond {.inspect}.
46
46
  #
47
47
  # @param url [String, Html2rss::Url] source page URL
48
- # @param strategy [Symbol] request strategy (:auto, :faraday, :botasaurus)
48
+ # @param strategy [Symbol] request strategy (:auto, :default, :botasaurus)
49
49
  # @return [Html2rss::Recon::Result]
50
50
  def self.recon(url, strategy: :auto, **)
51
51
  Recon.call(url, strategy:, **)
@@ -56,7 +56,7 @@ module Html2rss # rubocop:disable Metrics/ModuleLength
56
56
  # Golden path step 3.
57
57
  #
58
58
  # @param url [String] source page URL
59
- # @param strategy [Symbol] request strategy (+:auto+, +:faraday+, +:botasaurus+)
59
+ # @param strategy [Symbol] request strategy (+:auto+, +:default+, +:botasaurus+)
60
60
  # @option options [String, nil] :items_selector optional CSS selector hint for items
61
61
  # @option options [Array<String>, nil] :topics optional directory topics override
62
62
  # @option options [String, nil] :title optional title override
@@ -140,7 +140,7 @@ module Html2rss # rubocop:disable Metrics/ModuleLength
140
140
  # Scrapes multiple URLs in parallel using auto-source article discovery.
141
141
  #
142
142
  # @param urls [Enumerable<String>] list of URLs to scrape
143
- # @param strategy [Symbol] request strategy (:auto, :faraday, :botasaurus)
143
+ # @param strategy [Symbol] request strategy (:auto, :default, :botasaurus)
144
144
  # @param limit [Integer] max articles to keep per URL (default: 10)
145
145
  # @param concurrency [Integer] max worker threads (default: 5)
146
146
  # @return [Html2rss::Batch::BatchResult]
@@ -152,7 +152,7 @@ module Html2rss # rubocop:disable Metrics/ModuleLength
152
152
  # Inspects multiple URLs in parallel with per-URL error isolation.
153
153
  #
154
154
  # @param urls [Enumerable<String>] list of URLs to inspect
155
- # @param strategy [Symbol] request strategy (:auto, :faraday, :botasaurus)
155
+ # @param strategy [Symbol] request strategy (:auto, :default, :botasaurus)
156
156
  # @param concurrency [Integer] max worker threads (default: 5)
157
157
  # @return [Html2rss::Batch::BatchResult]
158
158
  def self.batch_inspect(urls, strategy: :auto, concurrency: Batch::DEFAULT_CONCURRENCY)
@@ -163,7 +163,7 @@ module Html2rss # rubocop:disable Metrics/ModuleLength
163
163
  # Runs recon across multiple URLs in parallel with per-URL error isolation.
164
164
  #
165
165
  # @param urls [Enumerable<String>] list of URLs to recon
166
- # @param strategy [Symbol] request strategy (:auto, :faraday, :botasaurus)
166
+ # @param strategy [Symbol] request strategy (:auto, :default, :botasaurus)
167
167
  # @param concurrency [Integer] max worker threads (default: 5)
168
168
  # @option options [String, nil] :cache_dir optional HTML cache directory
169
169
  # @return [Html2rss::Batch::BatchResult]
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.29.1
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gil Desmarais
@@ -52,53 +52,19 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
54
  - !ruby/object:Gem::Dependency
55
- name: faraday
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">"
59
- - !ruby/object:Gem::Version
60
- version: 2.0.1
61
- - - "<"
62
- - !ruby/object:Gem::Version
63
- version: '3.0'
64
- type: :runtime
65
- prerelease: false
66
- version_requirements: !ruby/object:Gem::Requirement
67
- requirements:
68
- - - ">"
69
- - !ruby/object:Gem::Version
70
- version: 2.0.1
71
- - - "<"
72
- - !ruby/object:Gem::Version
73
- version: '3.0'
74
- - !ruby/object:Gem::Dependency
75
- name: faraday-follow_redirects
76
- requirement: !ruby/object:Gem::Requirement
77
- requirements:
78
- - - ">="
79
- - !ruby/object:Gem::Version
80
- version: '0'
81
- type: :runtime
82
- prerelease: false
83
- version_requirements: !ruby/object:Gem::Requirement
84
- requirements:
85
- - - ">="
86
- - !ruby/object:Gem::Version
87
- version: '0'
88
- - !ruby/object:Gem::Dependency
89
- name: faraday-gzip
55
+ name: httpx
90
56
  requirement: !ruby/object:Gem::Requirement
91
57
  requirements:
92
58
  - - "~>"
93
59
  - !ruby/object:Gem::Version
94
- version: '3'
60
+ version: '1.8'
95
61
  type: :runtime
96
62
  prerelease: false
97
63
  version_requirements: !ruby/object:Gem::Requirement
98
64
  requirements:
99
65
  - - "~>"
100
66
  - !ruby/object:Gem::Version
101
- version: '3'
67
+ version: '1.8'
102
68
  - !ruby/object:Gem::Dependency
103
69
  name: kramdown
104
70
  requirement: !ruby/object:Gem::Requirement
@@ -431,7 +397,7 @@ files:
431
397
  - lib/html2rss/request_service/budget.rb
432
398
  - lib/html2rss/request_service/compressed_body.rb
433
399
  - lib/html2rss/request_service/context.rb
434
- - lib/html2rss/request_service/faraday_strategy.rb
400
+ - lib/html2rss/request_service/httpx_strategy.rb
435
401
  - lib/html2rss/request_service/local_file_strategy.rb
436
402
  - lib/html2rss/request_service/network_guard.rb
437
403
  - lib/html2rss/request_service/policy.rb
@@ -502,7 +468,7 @@ licenses:
502
468
  - MIT
503
469
  metadata:
504
470
  allowed_push_host: https://rubygems.org
505
- changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.29.1
471
+ changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.30.0
506
472
  rubygems_mfa_required: 'true'
507
473
  rdoc_options: []
508
474
  require_paths: