Surface real website-crawl failures instead of silently reporting fake success
Browse filesSame bug class as the earlier Drive fix: if the homepage itself couldn't be
reached or came back as a bot-challenge/CAPTCHA interstitial (which we
already confirmed ezofis.com's SiteGround WAF does to automated clients),
crawl_website silently returned an empty list, and reindex_website reported
"success, 0 pages crawled" with no indication anything was wrong.
crawl_website now fetches the homepage first, outside the tolerant BFS loop,
and raises a clear RuntimeError if it's unreachable or is a bot-challenge
page. Individual sub-page failures deeper in the crawl are still tolerated
(a normal, acceptable partial result) — only a broken homepage fetch is
treated as a real failure, surfaced via reindex_website's existing error
handling into the Chat Agent status log.
Verified: a simulated WAF-block (the exact SiteGround captcha page observed
from ezofis.com) now marks the log status "error" with an actionable
message; a normal successful crawl still indexes correctly (regression
check).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@@ -221,27 +221,54 @@ def crawl_website(base_url: str, max_pages: int = MAX_PAGES) -> List[Dict[str, s
|
|
| 221 |
so pages that aren't well-linked from nav still get discovered, then supplements via BFS
|
| 222 |
link-following from every fetched page.
|
| 223 |
Returns list of {"url": ..., "title": ..., "markdown": ...}.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
"""
|
| 225 |
base_url = (base_url or "").strip()
|
| 226 |
if not base_url.startswith(("http://", "https://")):
|
| 227 |
base_url = "https://" + base_url
|
| 228 |
parsed_root = urlparse(base_url)
|
| 229 |
if not parsed_root.netloc:
|
| 230 |
-
|
| 231 |
root_host = parsed_root.netloc
|
| 232 |
|
| 233 |
robots = _load_robots(base_url)
|
| 234 |
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
try:
|
| 237 |
sitemap_urls = _discover_sitemap_urls(base_url, root_host)
|
| 238 |
except Exception as e:
|
| 239 |
logger.warning("Sitemap discovery failed for %s: %s", base_url, e)
|
| 240 |
|
| 241 |
-
visited: Set[str] =
|
| 242 |
-
queue: List[str] = [
|
| 243 |
results: List[Dict[str, str]] = []
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
while queue and len(results) < max_pages:
|
| 246 |
url = queue.pop(0)
|
| 247 |
url, _ = urldefrag(url)
|
|
|
|
| 221 |
so pages that aren't well-linked from nav still get discovered, then supplements via BFS
|
| 222 |
link-following from every fetched page.
|
| 223 |
Returns list of {"url": ..., "title": ..., "markdown": ...}.
|
| 224 |
+
|
| 225 |
+
Raises RuntimeError if the homepage itself can't be reached or comes back as a bot-challenge
|
| 226 |
+
page — a caller treating that the same as "0 real pages found" would misreport a broken crawl
|
| 227 |
+
(e.g. blocked by a WAF) as a successful index of an empty site.
|
| 228 |
"""
|
| 229 |
base_url = (base_url or "").strip()
|
| 230 |
if not base_url.startswith(("http://", "https://")):
|
| 231 |
base_url = "https://" + base_url
|
| 232 |
parsed_root = urlparse(base_url)
|
| 233 |
if not parsed_root.netloc:
|
| 234 |
+
raise RuntimeError(f"'{base_url}' is not a valid URL")
|
| 235 |
root_host = parsed_root.netloc
|
| 236 |
|
| 237 |
robots = _load_robots(base_url)
|
| 238 |
|
| 239 |
+
# Fetch the homepage first, outside the tolerant loop below — if this specific fetch fails
|
| 240 |
+
# or is a bot-challenge page, the crawl is fundamentally broken, not just "site has 0 pages".
|
| 241 |
+
home_resp = _fetch(base_url)
|
| 242 |
+
if home_resp is None:
|
| 243 |
+
raise RuntimeError(
|
| 244 |
+
f"Could not reach {base_url} (network error, or a non-2xx/non-HTML response after retries)."
|
| 245 |
+
)
|
| 246 |
+
home_title, home_text, home_links = _extract_title_text_links(home_resp.text, base_url)
|
| 247 |
+
if _looks_like_bot_challenge(home_text):
|
| 248 |
+
raise RuntimeError(
|
| 249 |
+
f"{base_url} appears to be behind bot/CAPTCHA protection (Cloudflare, SiteGround, Sucuri, "
|
| 250 |
+
"etc.) that blocks automated crawling — it returned a bot-challenge page instead of real "
|
| 251 |
+
"content. Try allowlisting this server, or add this content via the Google Drive "
|
| 252 |
+
"knowledge base instead."
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
sitemap_urls: List[str] = []
|
| 256 |
try:
|
| 257 |
sitemap_urls = _discover_sitemap_urls(base_url, root_host)
|
| 258 |
except Exception as e:
|
| 259 |
logger.warning("Sitemap discovery failed for %s: %s", base_url, e)
|
| 260 |
|
| 261 |
+
visited: Set[str] = {base_url}
|
| 262 |
+
queue: List[str] = [u for u in sitemap_urls if u != base_url]
|
| 263 |
results: List[Dict[str, str]] = []
|
| 264 |
|
| 265 |
+
if len(home_text) >= 60:
|
| 266 |
+
markdown = f"# {home_title or base_url}\n\nSource: {base_url}\n\n{home_text[:20000]}"
|
| 267 |
+
results.append({"url": base_url, "title": home_title or base_url, "markdown": markdown})
|
| 268 |
+
for link in home_links:
|
| 269 |
+
if link not in visited:
|
| 270 |
+
queue.append(link)
|
| 271 |
+
|
| 272 |
while queue and len(results) < max_pages:
|
| 273 |
url = queue.pop(0)
|
| 274 |
url, _ = urldefrag(url)
|