Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Avoiding traps

A hidden link can lead a crawler into an endless branch of generated pages. Spire addresses the two parts separately: link filtering skips hidden entry points, and frontier limits bound URL expansion after a crawler enters a branch.

Keep obeying robots.txt: a site may disallow its trap paths there. Those rules can prevent entry, but do not detect traps when the site has not declared them.

FollowLinks skips hidden links by default, including when narrowed with select(...). When following links yourself, apply visible():

use spire::Result;
use spire::extract::html::Links;
use spire::frontier::queue::Queue;

async fn page(links: Links, queue: Queue) -> Result<()> {
    queue.follow_all(links.visible().same_host()).await?;
    Ok(())
}

HTTP sessions estimate visibility from attributes and inline CSS. Browser sessions use computed styles and layout through their live Document capability, so stylesheet rules and script changes are accounted for. Links below the fold remain followable.

Unknown visibility is kept: an HTTP session cannot establish what a full browser would render. Raw iteration of Links and manual Queue::follow do not apply visibility filtering. FollowLinks::include_hidden() opts out explicitly. See Hidden links for the checks and their limits, including HTML parsed from a browser snapshot.

Bound expanding branches

A trap can also have visible links. Add avoid_traps() to bound each URL prefix independently, while allowing the rest of the crawl to continue:

use std::sync::Arc;

use spire::backend::ReqwestBackend;
use spire::crawler::Robots;
use spire::frontier::seed::Seed;
use spire::frontier::{FrontierExt, InMemFrontier};
use spire::handler::FollowLinks;
use spire::{Crawler, Result, Router};

async fn crawl() -> Result<()> {
let traps = Arc::new(InMemFrontier::fifo()
    .dedup()
    .retry(3)
    .avoid_traps());
let frontier = traps.clone().allow_hosts(["example.com"]);
let crawler = Crawler::new(
    ReqwestBackend::new(),
    Router::new().fallback(FollowLinks::new("page")),
)
.with_frontier(frontier)
.with_robots(Robots::new("MyBot"))
.with_seed(Seed::get("https://example.com/")?);

crawler.run().await?;
println!("{} candidates refused or queued seeds skipped", traps.refused());
Ok(())
}

The facade checks candidate counts, directory fan-out, path depth, repeated segments and opaque-looking path segments. Its default scope is an origin and first path segment: /archive/ and /products/ have separate budgets. Put it outside retry/revisit facades to check their queued work, and inside scope filters so out-of-scope links are rejected first.

Tune limits for the site

An archive can be deep and large; product IDs can look random. These patterns alone do not prove that a branch is a trap. TrapPolicy lets you raise budgets or disable individual checks for a known site. See the frontier’s trap policy for defaults, prefix rules and configuration examples.

Budget exhaustion returns Admission::PrefixBudgetExceeded; suspicious path patterns return Admission::SuspectedTrap. A blocked prefix refuses further candidates and skips queued seeds, while in-flight requests may finish. refused() counts both kinds of refusal separately from crawler request totals. Tracing reports which prefix was blocked and why.

Trap state is in memory and resets when the facade is recreated. These checks bound crawling; they do not inspect generated text or establish whether collected content is trustworthy. Keep ordinary request and time budgets as an overall bound too.