http_mimic 0.3.0 → 0.3.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: 32ae2902090ddf216e6f33843f1fb53d82d13cda864a3a71fd7318f5f316cdf4
4
- data.tar.gz: 907aec51da124505902e104ebf3bce70eb2883490ec8b360f3f0571e6efd98c2
3
+ metadata.gz: 0b09482a3e618dd6390934e85d83de762cffaa062d5610d606d77bdabfb0ff11
4
+ data.tar.gz: 55c4dfeee202c24898a1ad5a12bc5a58df9c5a1e0481b7d08be0fc0c4a909e32
5
5
  SHA512:
6
- metadata.gz: e47bf2078c03dc6a37ecb3478ee5f7a42724375161c63175067fd6df5c8c76fae90b04867eb6f4b8fe03ab2ad2d919ef64dcfd1389ab20befdcb9e2ff245d492
7
- data.tar.gz: 31c4ebd49e1180914bd949dc23bbc4bd7580dea84f898c7a97bc4977a8501120b1d3d5e6301b65ef2bf7cfa9e5c9bbecd8a7f47806f0a8cf42e1e1b4e7b2e45c
6
+ metadata.gz: 1b4a75f41696e4b6a89c73096640ce5f9b3963dff9262aea51ead8271022868397e7c413edfc724cd7c0cbb439a2663bf55f5ea28cd060e491cff0445e88fea7
7
+ data.tar.gz: cf2f82e990474a02e00aebe5d5b0b0ca019a994266279cd84f7fd4dd966e9260b488e1915f2b7eccdf7452e50a838489d4709181ec195bebbc773e6aff62d6d8
data/CHANGELOG.md CHANGED
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.1] - 2026-08-25
9
+
10
+ ### Added
11
+ - **Smart Adaptive Multi-Strategy Modes (`:auto`, `:impersonate_first`, `:curl_first`, `:impersonate_only`, `:curl_only`)**:
12
+ - **Zero-Configuration Protection Bypass (`:auto` mode, enabled by default)**: Seamlessly tries `curl-impersonate` first, and if blocked by WAFs with `403`/`429`/`503` (e.g. Akamai bot challenges requiring JS), automatically falls back to standard `curl` with server client headers to reliably retrieve 200 OK.
13
+ - Per-request and class-level configurable execution mode (`mode:`, `auto_fallback:`, `retry_statuses:`).
14
+ - Enhanced `Response` object with `response.mode_used`, `response.fallback_triggered?`, and `response.attempts` metadata.
15
+ - Added live integration test for Akamai-protected site bypass (`test_smart_auto_fallback_on_akamai_protected_site`).
16
+
8
17
  ## [0.3.0] - 2026-08-24
9
18
 
10
19
  ### Added
data/README.md CHANGED
@@ -142,6 +142,7 @@ puts "User-Agent: #{res['user_agent']}"
142
142
  ```ruby
143
143
  client = HttpMimic::Client.new(
144
144
  base_uri: 'https://api.example.com',
145
+ mode: :auto,
145
146
  impersonate: 'firefox135',
146
147
  timeout: 15,
147
148
  headers: {
@@ -158,12 +159,41 @@ response = client.post('/v1/users', json: { username: 'bob' })
158
159
 
159
160
  ---
160
161
 
162
+ ## 🧠 Smart Adaptive Modes (Zero-Configuration Scraping)
163
+
164
+ `HttpMimic` provides built-in multi-strategy orchestration so you can fetch protected websites without worrying about which specific WAF (Cloudflare, Akamai, DataDome) protects them:
165
+
166
+ - **`:auto` (Default & Recommended)**: Tries `curl-impersonate` (Chrome 131) first. If blocked by WAFs like Akamai with `403`/`429`/`503` (which require JS telemetry for browsers but allow standard server clients), it **automatically and seamlessly retries with standard curl + server headers**, directly returning `200 OK`.
167
+ - **`:impersonate_first`**: Prefers `curl-impersonate` and automatically falls back to standard `curl` if blocked.
168
+ - **`:curl_first`**: Prefers standard `curl` and automatically upgrades to `curl-impersonate` if blocked.
169
+ - **`:impersonate_only`**: Strictly uses `curl-impersonate` (no retry/fallback).
170
+ - **`:curl_only`**: Strictly uses standard `curl` (no retry/fallback).
171
+
172
+ ```ruby
173
+ # 1. No-Brain Auto Mode (Works automatically for both Cloudflare & Akamai):
174
+ response = HttpMimic.get('https://www.asics.com/us/en-us/gt-2000-15/p/ANA_1011C235-750.html')
175
+ puts response.code # => 200
176
+ puts response.mode_used # => :curl
177
+ puts response.fallback_triggered? # => true
178
+
179
+ # 2. Per-request mode override:
180
+ response = HttpMimic.get(url, mode: :curl_first)
181
+ response = HttpMimic.get(url, mode: :impersonate_only)
182
+ ```
183
+
184
+ ---
185
+
161
186
  ## ⚙️ Global Configuration
162
187
 
163
188
  Configure global defaults in an initializer (e.g., `config/initializers/http_mimic.rb`):
164
189
 
165
190
  ```ruby
166
191
  HttpMimic.configure do |config|
192
+ # Multi-strategy & Smart Fallback
193
+ config.mode = :auto # :auto (default), :impersonate_first, :curl_first, :impersonate_only, :curl_only
194
+ config.auto_fallback = true # Automatically retry with alternative profile if blocked
195
+ config.retry_statuses = [403, 429, 503] # Status codes that trigger auto-fallback
196
+
167
197
  # Browser simulation & request defaults
168
198
  config.default_impersonate = 'chrome131' # Default browser target
169
199
  config.default_timeout = 30 # Request timeout (seconds)
@@ -188,6 +218,9 @@ end
188
218
 
189
219
  | Option | Type | Description |
190
220
  | :--- | :--- | :--- |
221
+ | `:mode` | Symbol | Execution strategy: `:auto` (default), `:impersonate_first`, `:curl_first`, `:impersonate_only`, `:curl_only` |
222
+ | `:auto_fallback` | Boolean | Whether to automatically retry with alternative profile on blocked status (default `true`) |
223
+ | `:retry_statuses`| Array | Status codes that trigger auto-fallback (default `[403, 429, 503]`) |
191
224
  | `:impersonate` | String | Target browser to mimic (e.g., `'chrome131'`, `'chrome120'`, `'firefox135'`, `'safari180'`, `'tor145'`) |
192
225
  | `:binary` | String | Path to a custom `curl-impersonate` executable |
193
226
  | `:query` / `:params` | Hash | URL query parameters (supports nested parameters and encoding) |
@@ -234,10 +267,13 @@ response.headers['content-type'] # => Case-insensitive header access
234
267
  response.cookies['session_id'] # => Parsed Set-Cookie store
235
268
  response.history # => Array of redirect history metadata
236
269
 
237
- # Underlying execution details
238
- response.exit_code # => Process exit status (0 for success)
239
- response.stderr # => Stderr output from curl
240
- response.command # => Array of the exact CLI arguments executed
270
+ # Underlying execution & multi-strategy details
271
+ response.mode_used # => :impersonate or :curl
272
+ response.fallback_triggered? # => true if smart fallback was executed
273
+ response.attempts # => Array of execution metadata for each attempt
274
+ response.exit_code # => Process exit status (0 for success)
275
+ response.stderr # => Stderr output from curl
276
+ response.command # => Array of the exact CLI arguments executed
241
277
  ```
242
278
 
243
279
  ---
data/http_mimic.gemspec CHANGED
@@ -14,7 +14,6 @@ Gem::Specification.new do |spec|
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.metadata = {
17
- "homepage_uri" => "https://github.com/anxgang/http_mimic",
18
17
  "source_code_uri" => "https://github.com/anxgang/http_mimic",
19
18
  "bug_tracker_uri" => "https://github.com/anxgang/http_mimic/issues",
20
19
  "changelog_uri" => "https://github.com/anxgang/http_mimic/blob/main/CHANGELOG.md"
@@ -171,6 +171,15 @@ module HttpMimic
171
171
  args << '-A' << options[:user_agent].to_s
172
172
  end
173
173
 
174
+ # Profile-specific headers & HTTP version defaults (for server/curl mode)
175
+ if options[:profile] == :curl
176
+ args << '--http1.1' unless (options[:curl_options] || []).to_s.include?('--http')
177
+ headers_to_send['User-Agent'] ||= (options[:user_agent] || 'Ruby')
178
+ headers_to_send['Accept'] ||= '*/*'
179
+ headers_to_send['Accept-Encoding'] ||= 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3'
180
+ headers_to_send['Connection'] ||= 'close'
181
+ end
182
+
174
183
  # Append headers
175
184
  headers_to_send.each do |k, v|
176
185
  if v.is_a?(Array)
@@ -194,6 +203,13 @@ module HttpMimic
194
203
  end
195
204
 
196
205
  def resolve_binary
206
+ # 0. If profile is explicitly set to :curl, use system curl
207
+ if options[:profile] == :curl
208
+ curl_path = find_executable('curl') || 'curl'
209
+ return curl_path if executable?(curl_path)
210
+ raise BinaryNotFoundError, "System curl executable was not found."
211
+ end
212
+
197
213
  # 1. Use explicit binary_path from options or config if provided
198
214
  explicit = options[:binary] || config.binary_path
199
215
  if explicit
@@ -13,6 +13,11 @@ module HttpMimic
13
13
  attr_accessor :logger
14
14
  attr_accessor :debug
15
15
 
16
+ # Multi-strategy and smart auto-fallback settings
17
+ attr_accessor :mode
18
+ attr_accessor :auto_fallback
19
+ attr_accessor :retry_statuses
20
+
16
21
  # Webdrivers-like auto download and driver management settings
17
22
  attr_accessor :auto_download
18
23
  attr_accessor :driver_version
@@ -31,6 +36,11 @@ module HttpMimic
31
36
  @logger = nil
32
37
  @debug = false
33
38
 
39
+ # Multi-strategy defaults: :auto seamlessly falls back between impersonate and curl
40
+ @mode = :auto
41
+ @auto_fallback = true
42
+ @retry_statuses = [403, 429, 503]
43
+
34
44
  # Enable automatic downloading of curl-impersonate driver (lexiforest) by default
35
45
  @auto_download = true
36
46
  @driver_version = 'v2.1.1'
@@ -41,6 +41,21 @@ module HttpMimic
41
41
  default_options[:cookies].merge!(c)
42
42
  end
43
43
 
44
+ def mode(m = nil)
45
+ return default_options[:mode] if m.nil?
46
+ default_options[:mode] = m
47
+ end
48
+
49
+ def auto_fallback(enabled = nil)
50
+ return default_options[:auto_fallback] if enabled.nil?
51
+ default_options[:auto_fallback] = enabled
52
+ end
53
+
54
+ def retry_statuses(statuses = nil)
55
+ return default_options[:retry_statuses] if statuses.nil?
56
+ default_options[:retry_statuses] = statuses
57
+ end
58
+
44
59
  def default_options
45
60
  @default_options ||= {}
46
61
  end
@@ -14,34 +14,89 @@ module HttpMimic
14
14
  end
15
15
 
16
16
  def perform
17
- builder = CommandBuilder.new(method, url, options, config)
18
- binary, args, stdin_data, final_url = builder.build
19
-
20
- full_command = [binary] + args
21
-
22
- log_debug("Executing HttpMimic command: #{full_command.join(' ')}")
23
- log_debug("Stdin data: #{stdin_data}") if stdin_data
24
-
25
- stdout, stderr, status = execute_open3(full_command, stdin_data)
26
-
27
- log_debug("Curl exit status: #{status.exitstatus}")
28
- log_debug("Curl stderr: #{stderr}") unless stderr.empty?
29
-
30
- response = ResponseParser.new(
31
- stdout,
32
- exit_status: status,
33
- stderr: stderr,
34
- command: full_command,
35
- request_url: final_url
36
- ).parse
17
+ mode = (options[:mode] || config.mode || :auto).to_sym
18
+ auto_fallback = options.fetch(:auto_fallback, config.auto_fallback)
19
+ retry_statuses = options[:retry_statuses] || config.retry_statuses || [403, 429, 503]
20
+
21
+ attempts = []
22
+ profiles_to_try = determine_profiles(mode, auto_fallback)
23
+
24
+ response = nil
25
+ final_status = nil
26
+ final_stderr = nil
27
+ final_command = nil
28
+
29
+ profiles_to_try.each_with_index do |profile, index|
30
+ current_opts = options.merge(profile: profile)
31
+
32
+ builder = CommandBuilder.new(method, url, current_opts, config)
33
+ binary, args, stdin_data, final_url = builder.build
34
+ full_command = [binary] + args
35
+
36
+ log_debug("Executing HttpMimic command (attempt #{index + 1}, profile: #{profile}): #{full_command.join(' ')}")
37
+ log_debug("Stdin data: #{stdin_data}") if stdin_data
38
+
39
+ stdout, stderr, status = execute_open3(full_command, stdin_data)
40
+
41
+ log_debug("Curl exit status: #{status.exitstatus}")
42
+ log_debug("Curl stderr: #{stderr}") unless stderr.empty?
43
+
44
+ response = ResponseParser.new(
45
+ stdout,
46
+ exit_status: status,
47
+ stderr: stderr,
48
+ command: full_command,
49
+ request_url: final_url
50
+ ).parse
51
+
52
+ response.mode_used = profile
53
+ response.fallback_triggered = (index > 0)
54
+
55
+ attempts << {
56
+ attempt: index + 1,
57
+ profile: profile,
58
+ command: full_command,
59
+ code: response.code,
60
+ exit_code: status.exitstatus,
61
+ success: response.success?
62
+ }
63
+ response.attempts = attempts
64
+
65
+ final_status = status
66
+ final_stderr = stderr
67
+ final_command = full_command
68
+
69
+ is_blocked = (status.exitstatus != 0) || retry_statuses.include?(response.code)
70
+ if !is_blocked || (index == profiles_to_try.size - 1)
71
+ break
72
+ end
37
73
 
38
- handle_errors(status, stderr, full_command, response)
74
+ log_debug("[HttpMimic] Attempt #{index + 1} with #{profile} resulted in status #{response.code}. Triggering smart fallback to next profile...")
75
+ end
39
76
 
77
+ handle_errors(final_status, final_stderr, final_command, response)
40
78
  response
41
79
  end
42
80
 
43
81
  private
44
82
 
83
+ def determine_profiles(mode, auto_fallback)
84
+ case mode
85
+ when :curl, :curl_first
86
+ auto_fallback ? [:curl, :impersonate] : [:curl]
87
+ when :curl_only
88
+ [:curl]
89
+ when :impersonate_only
90
+ [:impersonate]
91
+ when :impersonate_first
92
+ auto_fallback ? [:impersonate, :curl] : [:impersonate]
93
+ when :auto, :smart
94
+ auto_fallback ? [:impersonate, :curl] : [:impersonate]
95
+ else
96
+ auto_fallback ? [:impersonate, :curl] : [:impersonate]
97
+ end
98
+ end
99
+
45
100
  def execute_open3(command_array, stdin_data)
46
101
  if stdin_data
47
102
  Open3.capture3(*command_array, stdin_data: stdin_data)
@@ -54,6 +109,7 @@ module HttpMimic
54
109
 
55
110
  def handle_errors(status, stderr, command, response)
56
111
  raise_error = options.fetch(:raise_on_error, config.raise_on_error)
112
+ return unless status
57
113
  exit_code = status.exitstatus
58
114
 
59
115
  if exit_code != 0
@@ -72,7 +128,7 @@ module HttpMimic
72
128
  if raise_error
73
129
  raise error_class.new(message, exit_code: exit_code, stderr: stderr, command: command, response: response)
74
130
  end
75
- elsif raise_error && response.error?
131
+ elsif raise_error && response&.error?
76
132
  raise CommandError.new("HTTP request failed (status #{response.code})", exit_code: exit_code, stderr: stderr, command: command, response: response)
77
133
  end
78
134
  end
@@ -16,22 +16,34 @@ module HttpMimic
16
16
  :exit_code,
17
17
  :command
18
18
 
19
+ attr_accessor :mode_used,
20
+ :fallback_triggered,
21
+ :attempts
22
+
19
23
  alias status code
24
+ alias strategy_used mode_used
20
25
 
21
26
  def initialize(attributes = {})
22
- @code = attributes[:code] || 0
23
- @http_version = attributes[:http_version]
24
- @status_message = attributes[:status_message]
25
- @headers = attributes[:headers] || Headers.new
26
- @cookies = attributes[:cookies] || Cookies.new
27
- @body = attributes[:body] || ''
28
- @parsed_response = attributes[:parsed_response]
29
- @raw_headers = attributes[:raw_headers] || ''
30
- @history = attributes[:history] || []
31
- @request_url = attributes[:request_url]
32
- @stderr = attributes[:stderr] || ''
33
- @exit_code = attributes[:exit_code] || 0
34
- @command = attributes[:command]
27
+ @code = attributes[:code] || 0
28
+ @http_version = attributes[:http_version]
29
+ @status_message = attributes[:status_message]
30
+ @headers = attributes[:headers] || Headers.new
31
+ @cookies = attributes[:cookies] || Cookies.new
32
+ @body = attributes[:body] || ''
33
+ @parsed_response = attributes[:parsed_response]
34
+ @raw_headers = attributes[:raw_headers] || ''
35
+ @history = attributes[:history] || []
36
+ @request_url = attributes[:request_url]
37
+ @stderr = attributes[:stderr] || ''
38
+ @exit_code = attributes[:exit_code] || 0
39
+ @command = attributes[:command]
40
+ @mode_used = attributes[:mode_used] || :impersonate
41
+ @fallback_triggered = attributes[:fallback_triggered] || false
42
+ @attempts = attributes[:attempts] || []
43
+ end
44
+
45
+ def fallback_triggered?
46
+ !!@fallback_triggered
35
47
  end
36
48
 
37
49
  def success?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HttpMimic
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http_mimic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - anxgang
@@ -78,7 +78,6 @@ homepage: https://github.com/anxgang/http_mimic
78
78
  licenses:
79
79
  - MIT
80
80
  metadata:
81
- homepage_uri: https://github.com/anxgang/http_mimic
82
81
  source_code_uri: https://github.com/anxgang/http_mimic
83
82
  bug_tracker_uri: https://github.com/anxgang/http_mimic/issues
84
83
  changelog_uri: https://github.com/anxgang/http_mimic/blob/main/CHANGELOG.md