sigoden/aichat · warning · anyhow::Error
Invalid crawl page at
Error message
Invalid crawl page at {} What it means
In `crawl_website`, discovered relative link paths are resolved against the normalized start URL with `Url::join`; when `join` fails for a path (malformed or otherwise unresolvable URL), the crawl task returns `anyhow!("Invalid crawl page at {}", path)`. The failure occurs before `crawl_page` runs, so the page is never fetched.
Solutions
- Filter candidate links before enqueueing: keep only those that `Url::parse`/`join` successfully and whose scheme is http/https.
- Sanitize hrefs on the crawled page (strip `javascript:`, `data:`, `mailto:` schemes) before starting the crawl.
- Catch this error per-page and log a warning instead of failing the whole crawl.
- If you control the source pages, fix the malformed links at the origin.
Example fix
// before
async move {
let _permit = permit.await?;
let url = normalized_start_url.join(&path)
.map_err(|_| anyhow!("Invalid crawl page at {}", path))?;
// ...
}
// after
async move {
let Ok(url) = normalized_start_url.join(&path) else {
eprintln!("skipping unparseable link: {path}");
return Ok(None);
};
if !matches!(url.scheme(), "http" | "https") {
return Ok(None);
}
// ...
} Defensive patterns
Strategy: validation
Validate before calling
fn crawlable(path: &str, base: &url::Url) -> Option<url::Url> {
let url = base.join(path).ok()?;
matches!(url.scheme(), "http" | "https").then_some(url)
} Try / catch
// when driving crawl tasks yourself:
match tokio::join!(handle) {
Err(e) if e.to_string().starts_with("Invalid crawl page at") => {
log::warn!("skipped bad link: {e}");
}
other => other?,
} Prevention
- Filter hrefs to http/https schemes before enqueueing crawl pages.
- Drop javascript:, data:, mailto:, and empty anchors during link extraction.
- Pre-resolve every candidate URL with Url::join and skip failures.
- Fix malformed links at the source site where you control it.
When it happens
Trigger: The crawl discovers a link whose path cannot be joined onto `normalized_start_url` — e.g. a malformed href, a `data:`/`javascript:` pseudo-URL leaking into the path list, or an invalid percent-encoding.
Common situations: Crawling HTML pages with broken or exotic hrefs; scraping sites that emit `javascript:void(0)` or empty anchors as links; malformed sitemap entries.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
AI-assisted analysis of sigoden/aichat@82976d349a (2026-09-09).
Data as JSON: /api/errors/b09d66c80e3734e4.
Report an issue: GitHub.
Appendix: source
Thrown at src/utils/request.rs:250
let mut result_pages = Vec::new();
let mut index = 0;
while index < paths.len() {
let batch = paths[index..std::cmp::min(index + MAX_CRAWLS, paths.len())].to_vec();
let tasks: Vec<_> = batch
.iter()
.map(|path| {
let options = options.clone();
let permit = semaphore.clone().acquire_owned(); // acquire a permit for concurrency control
let normalized_start_url = normalized_start_url.clone();
let path = path.clone();
async move {
let _permit = permit.await?;
let url = normalized_start_url
.join(&path)
.map_err(|_| anyhow!("Invalid crawl page at {}", path))?;
let mut page = crawl_page(&normalized_start_url, &path, options)
.await
.with_context(|| format!("Failed to crawl {}", url.as_str()))?;
page.0 = url.as_str().to_string();
Ok(page)
}
})
.collect();
let results = stream::iter(tasks)
.buffer_unordered(MAX_CRAWLS)
.collect::<Vec<_>>()
.await;
let mut new_paths = Vec::new();
for res in results {
match res {View on GitHub (pinned to 82976d349a)