ask-web-fetch 0.7.5 → 0.7.7
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/ask/web_fetch/backend.rb +22 -7
- data/lib/ask/web_fetch/backends/browser.rb +9 -5
- data/lib/ask/web_fetch/backends/jina.rb +10 -6
- data/lib/ask/web_fetch/backends/local.rb +5 -3
- data/lib/ask/web_fetch/http.rb +5 -5
- data/lib/ask/web_fetch/version.rb +1 -1
- data/lib/ask/web_fetch.rb +10 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e1fdcc511917b85bfe1ab456733d89e09a24eb4dd4bd085d61ebcf5d52ff4d0
|
|
4
|
+
data.tar.gz: 59365989bd571bd06454513328a5f27c64116f6ced3669bcebb753c94fddfcee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ab5dfc8d25e5c9550a08981382a041e148291b6d83ab40c9f375404c7efd35f08d08b6444b10fabe4a8035773ea49606dc96a6c73211aeb7cd4f89e529ca359
|
|
7
|
+
data.tar.gz: 742d576e4a4f0a71ab89c47d177107f991c384795eef0775fc8579c96d6cbb41688cb30cabfb13d4d861bf6e665c54ee662998e44a66da64904955406562818f
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -45,7 +45,7 @@ module Ask
|
|
|
45
45
|
class Browser < Backend
|
|
46
46
|
# Seconds to let a Cloudflare-style challenge auto-solve before
|
|
47
47
|
# giving up (tunable via Browser.challenge_timeout).
|
|
48
|
-
CHALLENGE_TIMEOUT =
|
|
48
|
+
CHALLENGE_TIMEOUT = 10
|
|
49
49
|
|
|
50
50
|
# Seconds to wait for the network to go quiet after the page loads,
|
|
51
51
|
# so lazy-loaded content is in before we read the DOM.
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -15,8 +15,8 @@ module Ask
|
|
|
15
15
|
# pages that the Local backend cannot.
|
|
16
16
|
class Jina < Backend
|
|
17
17
|
BASE_URL = 'https://r.jina.ai'
|
|
18
|
-
OPEN_TIMEOUT =
|
|
19
|
-
READ_TIMEOUT =
|
|
18
|
+
OPEN_TIMEOUT = 3
|
|
19
|
+
READ_TIMEOUT = 10
|
|
20
20
|
|
|
21
21
|
def fetch(url)
|
|
22
22
|
uri = URI("#{BASE_URL}/#{url}")
|
|
@@ -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
|
|
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
|
|
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}",
|
|
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)
|
data/lib/ask/web_fetch/http.rb
CHANGED
|
@@ -18,12 +18,12 @@ module Ask
|
|
|
18
18
|
# TLS) all surface as Ask::WebFetch::TimeoutError, the transient
|
|
19
19
|
# bucket, whatever their underlying class.
|
|
20
20
|
class Http
|
|
21
|
-
CONNECT_TIMEOUT =
|
|
22
|
-
READ_TIMEOUT =
|
|
23
|
-
WRITE_TIMEOUT =
|
|
24
|
-
# Whole-request cap. A page that can't be read in
|
|
21
|
+
CONNECT_TIMEOUT = 3
|
|
22
|
+
READ_TIMEOUT = 8
|
|
23
|
+
WRITE_TIMEOUT = 8
|
|
24
|
+
# Whole-request cap. A page that can't be read in 20s is a
|
|
25
25
|
# problem page, not a stall worth a crawl worker.
|
|
26
|
-
OPERATION_TIMEOUT =
|
|
26
|
+
OPERATION_TIMEOUT = 20
|
|
27
27
|
# Retries per request, on top of the crawl ledger's own auto-heal
|
|
28
28
|
# rounds. GETs are idempotent; a couple of cheap retries beat a full
|
|
29
29
|
# ledger round-trip for transient flakiness.
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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:
|