ask-web-fetch 0.7.4 → 0.7.6

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: 412118d0b9d8d15d1677f877dcdd06cc3ca3d64f6afb79be80ae7fb404bdc8c5
4
- data.tar.gz: 18a022bec912f32ecd7f1afca82c11e7e5f016199779b92fec86ec5deb1428a7
3
+ metadata.gz: ae762437d9eb577d28a7f88d7cc82e5cb872e29a572c8bf3ec977f2602e2d398
4
+ data.tar.gz: 4c52bfb0d6a69d5184eea6ad81281dbea87f3bdfcd2a72219de3f95bbcd4b524
5
5
  SHA512:
6
- metadata.gz: b12c4fbcc8e2f76d1b76b07836e1c7aeaa735e22c75b085571a1a8f710425637b461a97f2ec5c073d6e88b1d843aeca9da50d2ecba36d72ad21fcd4da7926852
7
- data.tar.gz: 054750f93da3d1af328fb312867d26cbf4f7631133d9e79f76eb759816fd376e30b1a8df1b1b3f52d5752f8eac15aa38143c1c9484222543a20301c947ea2b4d
6
+ metadata.gz: 4be64a96e76ff2335e5d4e9ced1b6fc918eff6a8e5520b7f87660b554989e9ac6beb0841fcd12b218ac33c8776f9f9d3703c95da072b7f66e3ea3095916f66a6
7
+ data.tar.gz: 9741eeb18451170efedf920716dd22f1db61c57b71b43e76556d3e18f9303645671e6ee93867b78faefd8e026c2e272891856fc49e4ca95b91b3e791810dbac0
@@ -6,7 +6,14 @@ module Ask
6
6
  module WebFetch
7
7
  # Raised by backends on any failure; the tool catches it and tries the
8
8
  # next backend in the chain.
9
- class Error < StandardError; end
9
+ class Error < StandardError
10
+ attr_reader :hint
11
+
12
+ def initialize(message = nil, hint: nil)
13
+ @hint = hint
14
+ super(message)
15
+ end
16
+ end
10
17
 
11
18
  # A backend that failed to fetch because the URL itself is bad —
12
19
  # challenge page, non-HTML response, redirect loop. Deterministic:
@@ -14,17 +21,21 @@ module Ask
14
21
  class FetchError < Error
15
22
  attr_reader :status
16
23
 
17
- def initialize(message = nil, status: nil)
24
+ def initialize(message = nil, status: nil, hint: nil)
18
25
  @status = status
19
26
  msg = status ? "[#{status}] #{message}" : message
20
- super(msg)
27
+ super(msg, hint: hint)
21
28
  end
22
29
  end
23
30
 
24
31
  # HTTP 404 — the URL does not exist. Some 404 pages still carry
25
32
  # usable content (custom error pages with navigation, suggestions);
26
33
  # the backend tries to extract it before giving up.
27
- class NotFoundError < FetchError; end
34
+ class NotFoundError < FetchError
35
+ def initialize(message = nil, status: 404, hint: nil)
36
+ super(message, status: status, hint: hint)
37
+ end
38
+ end
28
39
 
29
40
  # A backend that fetched the page but found nothing usable in it.
30
41
  class EmptyContentError < Error; end
@@ -38,17 +49,21 @@ module Ask
38
49
 
39
50
  # Network-level failure — timeout, connection refused/reset, bad
40
51
  # socket. Transient: the same URL may succeed on retry.
41
- class TimeoutError < Error; end
52
+ class TimeoutError < Error
53
+ def initialize(message = nil, hint: nil)
54
+ super(message, hint: hint)
55
+ end
56
+ end
42
57
 
43
58
  # The service or the target server answered 5xx/429. Transient:
44
59
  # retrying after backoff may succeed.
45
60
  class ServerError < Error
46
61
  attr_reader :status
47
62
 
48
- def initialize(message = nil, status: nil)
63
+ def initialize(message = nil, status: nil, hint: nil)
49
64
  @status = status
50
65
  msg = status ? "[#{status}] #{message}" : message
51
- super(msg)
66
+ super(msg, hint: hint)
52
67
  end
53
68
  end
54
69
 
@@ -179,13 +179,17 @@ module Ask
179
179
  page = self.class.browser.create_page
180
180
  fetch_attempt(page, url)
181
181
  rescue Ferrum::TimeoutError, Ferrum::ProcessTimeoutError => e
182
- raise TimeoutError, "#{e.class}: #{e.message}"
182
+ raise TimeoutError.new("#{e.class}: #{e.message}",
183
+ hint: "page load timed out — site may be slow or blocking automation")
183
184
  rescue Ferrum::StatusError => e
184
- raise FetchError, "browser could not load #{url}: #{e.message}"
185
+ raise FetchError.new("browser could not load #{url}: #{e.message}",
186
+ hint: "navigation failed — URL may be invalid or unreachable")
185
187
  rescue Ferrum::Error => e
186
- raise ServerError, "browser #{e.class}: #{e.message}"
188
+ raise ServerError.new("browser #{e.class}: #{e.message}",
189
+ hint: "browser error — check Chrome/CDP connection")
187
190
  rescue Errno::ECONNREFUSED, SocketError => e
188
- raise TimeoutError, "browser connection #{e.class}: #{e.message}"
191
+ raise TimeoutError.new("browser connection #{e.class}: #{e.message}",
192
+ hint: "CDP endpoint unreachable — is Chrome running with --remote-debugging-port?")
189
193
  ensure
190
194
  page&.close
191
195
  end
@@ -49,18 +49,22 @@ module Ask
49
49
 
50
50
  { title: nil, description: nil, content: content, outlinks: markdown_outlinks(body, url) }
51
51
  when '429'
52
- raise ServerError.new('rate limited by Jina', status: 429)
52
+ raise ServerError.new('rate limited by Jina', status: 429,
53
+ hint: "Jina free tier limit hit — wait or set JINA_API_KEY")
53
54
  when '401', '403'
54
- raise FetchError.new("Jina access error", status: res.code.to_i)
55
+ raise FetchError.new("Jina access error", status: res.code.to_i,
56
+ hint: "site blocked automated requests or Jina denied access")
55
57
  else
56
58
  # 5xx = Jina-side blip (transient); other 4xx = the URL is dead.
57
59
  code = res.code.to_i
58
60
  error_class = code == 404 ? NotFoundError : (code >= 500 ? ServerError : FetchError)
59
- raise error_class.new("Jina returned #{res.code}", status: code)
61
+ raise error_class.new("Jina returned #{res.code}", status: code,
62
+ hint: code >= 500 ? "Jina-side error — try again later" : nil)
60
63
  end
61
64
  rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED,
62
65
  Errno::ECONNRESET, SocketError, URI::InvalidURIError => e
63
- raise TimeoutError, "Jina #{e.class}: #{e.message}"
66
+ raise TimeoutError.new("Jina #{e.class}: #{e.message}",
67
+ hint: "Jina service unreachable — try another backend")
64
68
  end
65
69
  end
66
70
  end
@@ -67,9 +67,11 @@ module Ask
67
67
  # Probe 3: full HTML scrape (legacy path)
68
68
  body, content_type, redirect, status = fetch_html(url)
69
69
  unless content_type.include?('html')
70
- raise FetchError.new("expected HTML from #{url}, got #{content_type}", status: status)
70
+ raise FetchError.new("expected HTML from #{url}, got #{content_type}", status: status,
71
+ hint: "server returned #{content_type} instead of HTML")
71
72
  end
72
- raise FetchError.new("challenge page at #{url}", status: status) if challenge_page?(body)
73
+ raise FetchError.new("challenge page at #{url}", status: status,
74
+ hint: "anti-bot challenge detected — try with CDP-attached browser") if challenge_page?(body)
73
75
 
74
76
  page = to_markdown(body, url)
75
77
  page[:redirected] = redirect
@@ -87,7 +89,7 @@ module Ask
87
89
  # up — only raise NotFoundError if the content is genuinely empty.
88
90
  if status == 404
89
91
  return page if usable_content?(page[:content])
90
- raise NotFoundError.new("not found at #{url}", status: 404)
92
+ raise NotFoundError.new("not found at #{url}", hint: "page does not exist")
91
93
  end
92
94
 
93
95
  guard_page!(url, page[:content], raw_body: body)
@@ -208,8 +210,14 @@ module Ask
208
210
  end
209
211
 
210
212
  unless (300..399).cover?(response.status) && !response.location.empty?
211
- # 4xx (other than 429) = the URL is dead; 429/5xx = transient.
212
213
  code = response.status
214
+ # 404: return the body so fetch() can try to extract content
215
+ # from custom error pages (some 404 pages carry navigation,
216
+ # search suggestions, or useful content).
217
+ if code == 404
218
+ return [response.body, response.content_type, redirect_info(first_hop_status, uri), code]
219
+ end
220
+ # 429/5xx = transient; other 4xx = the URL is dead.
213
221
  raise(code == 429 || code >= 500 ? ServerError : FetchError, "got #{code} from #{url}")
214
222
  end
215
223
  raise FetchError, "hit a redirect loop at #{url}" if (hops += 1) > MAX_REDIRECTS
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module WebFetch
5
- VERSION = '0.7.4'
5
+ VERSION = '0.7.6'
6
6
  end
7
7
  end
data/lib/ask/web_fetch.rb CHANGED
@@ -95,19 +95,25 @@ module Ask
95
95
  end
96
96
  message = "#{detail} #{url}"
97
97
 
98
+ # Attach the most informative hint — prefer the first backend's hint
99
+ # when all agree, otherwise use the first non-nil hint.
100
+ hints = failures.filter_map { |_, e| e.respond_to?(:hint) && e.hint }
101
+ hint = hints.uniq.size == 1 ? hints.first : hints.first
102
+
98
103
  classes = failures.map { |_, e| e.class }
99
104
  if classes.any? { |k| k <= Ask::WebFetch::ParkedDomainError }
100
- raise Ask::WebFetch::ParkedDomainError, message
105
+ raise Ask::WebFetch::ParkedDomainError.new(message, hint: hint)
101
106
  end
102
107
  if classes.any? { |k| k <= Ask::WebFetch::EmptyContentError }
103
- raise Ask::WebFetch::EmptyContentError, message
108
+ raise Ask::WebFetch::EmptyContentError.new(message, hint: hint)
104
109
  end
105
110
  if classes.any? { |k| k == Ask::WebFetch::NotFoundError }
106
- raise Ask::WebFetch::NotFoundError, message
111
+ raise Ask::WebFetch::NotFoundError.new(message, hint: hint)
107
112
  end
108
113
 
109
114
  deterministic = failures.all? { |_, e| DETERMINISTIC.any? { |klass| e.is_a?(klass) } }
110
- raise(deterministic ? Ask::WebFetch::FetchError : Ask::WebFetch::Error, message)
115
+ error_class = deterministic ? Ask::WebFetch::FetchError : Ask::WebFetch::Error
116
+ raise error_class.new(message, hint: hint)
111
117
  end
112
118
 
113
119
  # Fetches +url+ and returns LLM-ready markdown — "# Title\n\nSource:
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-web-fetch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.4
4
+ version: 0.7.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto