Session API

The Session API provides stateful web browsing with cookie persistence, form interaction, navigation history, and built-in web search. It is the primary interface for agents interacting with the web through browsy.

#![allow(unused)]
fn main() {
use browsy_core::fetch::Session;

let mut session = Session::new()?;
let dom = session.goto("https://example.com")?;
}

Requires the fetch feature (enabled by default).

Creating a session

Session::new()

Creates a session with default configuration (1920x1080 viewport, 30s timeout, CSS fetching enabled).

#![allow(unused)]
fn main() {
let mut session = Session::new()?;
}

Session::with_config(config)

Creates a session with custom configuration.

#![allow(unused)]
fn main() {
use browsy_core::fetch::{Session, SessionConfig};

let config = SessionConfig {
    viewport_width: 1366.0,
    viewport_height: 768.0,
    timeout_secs: 15,
    fetch_css: false,  // Skip external CSS for speed
    ..Default::default()
};
let mut session = Session::with_config(config)?;
}

SessionConfig fields

FieldTypeDefaultDescription
viewport_widthf321920.0Viewport width in pixels. Affects layout computation and fold detection
viewport_heightf321080.0Viewport height in pixels. Defines the fold line
user_agentStringChrome-like UAHTTP User-Agent header
timeout_secsu6430HTTP request timeout
fetch_cssbooltrueWhether to fetch external CSS stylesheets. Disabling speeds up parsing but reduces layout accuracy
blocked_patternsVec<String>Analytics/tracking URLsURL patterns to block (analytics, ads, tracking pixels)
max_response_bytesusize5MBMaximum HTML response size
max_css_bytes_totalusize2MBMaximum total CSS bytes across all stylesheets
max_css_bytes_per_fileusize512KBMaximum size per individual CSS file
max_redirectsusize10Maximum HTTP redirect chain length
allow_private_networkboolfalseWhether to allow requests to private/internal IPs
allow_non_httpboolfalseWhether to allow non-HTTP(S) schemes

goto(url) -> Result<SpatialDom, FetchError>

Navigate to a URL. Fetches the page, parses HTML, optionally fetches external CSS, computes layout, and returns the Spatial DOM. Cookies are persisted automatically.

#![allow(unused)]
fn main() {
let dom = session.goto("https://news.ycombinator.com")?;
println!("Title: {}", dom.title);
println!("Elements: {}", dom.els.len());
}

back() -> Result<SpatialDom, FetchError>

Navigate to the previous page in history. Returns an error if there is no history.

#![allow(unused)]
fn main() {
session.goto("https://example.com")?;
session.goto("https://example.com/about")?;
let dom = session.back()?;  // Back to example.com
}

url() -> Option<&str>

Returns the current page URL.

#![allow(unused)]
fn main() {
if let Some(url) = session.url() {
    println!("Currently at: {}", url);
}
}

Interaction

click(id) -> Result<SpatialDom, FetchError>

Click an element by ID. Behavior depends on the element type:

  • Links (<a>) -- navigates to the href URL. Skips javascript:, mailto:, tel:, and anchor-only (#) links.
  • Buttons / submit inputs -- submits the parent form with all current form values.
  • Elements with JS behaviors -- simulated. onclick handlers with window.location trigger navigation. Toggle/show/hide behaviors modify the DOM.
#![allow(unused)]
fn main() {
let dom = session.goto("https://news.ycombinator.com")?;
// Click the first link
let dom = session.click(3)?;
}

type_text(id, text) -> Result<(), FetchError>

Type text into an input or textarea. The value is stored in the session and overlaid onto the DOM. When a form is submitted via click, these values are included in the form data.

#![allow(unused)]
fn main() {
session.type_text(19, "user@example.com")?;
session.type_text(21, "hunter2")?;
}

Returns an error if the element is not an input or textarea.

check(id) -> Result<(), FetchError>

Check a checkbox or radio button.

#![allow(unused)]
fn main() {
session.check(36)?;  // Check "Remember me"
}

uncheck(id) -> Result<(), FetchError>

Uncheck a checkbox or radio button.

#![allow(unused)]
fn main() {
session.uncheck(36)?;
}

toggle(id) -> Result<(), FetchError>

Toggle a checkbox or radio button based on its current effective state (considering session overrides and HTML defaults).

#![allow(unused)]
fn main() {
session.toggle(36)?;  // If checked, unchecks. If unchecked, checks.
}

select(id, value) -> Result<(), FetchError>

Select an option in a <select> element by value.

#![allow(unused)]
fn main() {
session.select(15, "california")?;
}

Reading page state

dom() -> Option<SpatialDom>

Returns the current Spatial DOM with form state overlaid. Typed values, checked/unchecked states from type_text, check, and uncheck are reflected in the returned DOM.

#![allow(unused)]
fn main() {
session.type_text(19, "hello")?;
let dom = session.dom().unwrap();
let el = dom.get(19).unwrap();
assert_eq!(el.val.as_deref(), Some("hello"));
}

dom_ref() -> Option<&SpatialDom>

Returns a reference to the raw Spatial DOM without form state overlay. Reflects the page as parsed, ignoring any type_text/check/uncheck calls.

#![allow(unused)]
fn main() {
let raw = session.dom_ref().unwrap();
}

delta() -> Option<DeltaDom>

Returns the diff between the current and previous page. Only available after at least two navigations.

#![allow(unused)]
fn main() {
session.goto("https://example.com")?;
session.goto("https://example.com/about")?;
if let Some(delta) = session.delta() {
    println!("Added/changed: {}", delta.changed.len());
    println!("Removed IDs: {:?}", delta.removed);
}
}

element(id) -> Option<&SpatialElement>

O(1) element lookup by ID.

#![allow(unused)]
fn main() {
if let Some(el) = session.element(42) {
    println!("{}: {}", el.tag, el.text.as_deref().unwrap_or(""));
}
}

Finding elements

find_by_text(text) -> Vec<&SpatialElement>

Exact substring match on element text (case-sensitive).

#![allow(unused)]
fn main() {
let results = session.find_by_text("Sign in");
}

find_by_text_fuzzy(text) -> Vec<&SpatialElement>

Case-insensitive substring match on element text.

#![allow(unused)]
fn main() {
let results = session.find_by_text_fuzzy("sign in");
// Matches "Sign In", "SIGN IN", "Please sign in", etc.
}

find_by_role(role) -> Vec<&SpatialElement>

Find all elements with a specific ARIA role.

#![allow(unused)]
fn main() {
let headings = session.find_by_role("heading");
let links = session.find_by_role("link");
let buttons = session.find_by_role("button");
}

find_input_by_purpose(purpose) -> Option<&SpatialElement>

Find an input element by its semantic purpose. Matches on input type, name, label, and placeholder.

#![allow(unused)]
fn main() {
use browsy_core::fetch::InputPurpose;

let password = session.find_input_by_purpose(InputPurpose::Password);
let email = session.find_input_by_purpose(InputPurpose::Email);
let username = session.find_input_by_purpose(InputPurpose::Username);
let code = session.find_input_by_purpose(InputPurpose::VerificationCode);
let search = session.find_input_by_purpose(InputPurpose::Search);
let phone = session.find_input_by_purpose(InputPurpose::Phone);
}
PurposeMatching logic
Passwordinput[type="password"]
Emailinput[type="email"] or name/label contains email
UsernameText/email input with name/label containing user or login
VerificationCodeText/number/tel input with name/label/placeholder containing code, otp, or verify
Searchinput[type="search"], role searchbox, or name containing search
Phoneinput[type="tel"] or name/label containing phone

find_nearest_button(input_id) -> Option<&SpatialElement>

Find the nearest submit button to a given input element. Prefers buttons below the input, scored by Manhattan distance with Y weighted 2x.

#![allow(unused)]
fn main() {
if let Some(btn) = session.find_nearest_button(19) {
    println!("Submit button: {} (id: {})", btn.text.as_deref().unwrap_or(""), btn.id);
}
}

Compound actions

These methods combine multiple interactions into a single call, using the page intelligence action recipes.

login(username, password) -> Result<SpatialDom, FetchError>

Detects the login form from suggested_actions, fills in credentials, and submits. Returns the resulting page.

#![allow(unused)]
fn main() {
let dom = session.goto("https://github.com/login")?;
let result = session.login("user@example.com", "hunter2")?;
}

Returns an error if no Login action recipe was detected on the current page.

enter_code(code) -> Result<SpatialDom, FetchError>

Fills in a verification code and submits the form, using the EnterCode action recipe.

#![allow(unused)]
fn main() {
let result = session.enter_code("847291")?;
}

find_verification_code() -> Option<String>

Extracts a verification code from the current page text (4-8 digit sequences near code-related keywords).

#![allow(unused)]
fn main() {
// On a page that says "Your verification code is 847291"
if let Some(code) = session.find_verification_code() {
    session.enter_code(&code)?;
}
}

CAPTCHA detection

is_captcha() -> bool

Returns true if the current page is classified as a CAPTCHA challenge.

#![allow(unused)]
fn main() {
if session.is_captcha() {
    println!("CAPTCHA detected -- cannot proceed automatically");
}
}

captcha_info() -> Option<&CaptchaInfo>

Returns CAPTCHA details if detected: captcha_type (ReCaptcha, HCaptcha, Turnstile, CloudflareChallenge, ImageGrid, TextCaptcha, Unknown) and optional sitekey.

#![allow(unused)]
fn main() {
if let Some(info) = session.captcha_info() {
    match info.captcha_type {
        CaptchaType::ReCaptcha => {
            println!("reCAPTCHA sitekey: {:?}", info.sitekey);
        }
        CaptchaType::CloudflareChallenge => {
            println!("Cloudflare challenge -- wait and retry");
        }
        _ => {}
    }
}
}

search(query) -> Result<Vec<SearchResult>, FetchError>

Search the web using DuckDuckGo. Returns structured results with title, URL, and snippet.

#![allow(unused)]
fn main() {
let results = session.search("rust programming language")?;
for r in &results {
    println!("{}: {} -- {}", r.title, r.url, r.snippet);
}
}

search_with(query, engine) -> Result<Vec<SearchResult>, FetchError>

Search with a specific engine.

#![allow(unused)]
fn main() {
use browsy_core::fetch::SearchEngine;

let results = session.search_with("browsy", SearchEngine::Google)?;
}

Available engines: SearchEngine::DuckDuckGo (default, most reliable) and SearchEngine::Google (may return CAPTCHAs for automated requests).

search_and_read(query, n) -> Result<Vec<SearchPage>, FetchError>

Search and fetch the top N results, returning each page's Spatial DOM alongside the search result metadata.

#![allow(unused)]
fn main() {
let pages = session.search_and_read("rust web scraping", 3)?;
for page in &pages {
    println!("{}:", page.result.title);
    if let Some(ref dom) = page.dom {
        println!("  {} elements, page_type: {:?}", dom.els.len(), dom.page_type);
    }
}
}

Behaviors

behaviors() -> Vec<JsBehavior>

Detects JavaScript behaviors from HTML attributes (onclick, data-toggle, data-bs-toggle, etc.). Returns trigger element IDs and inferred actions.

#![allow(unused)]
fn main() {
let behaviors = session.behaviors();
for b in &behaviors {
    println!("Element {} triggers {:?}", b.trigger_id, b.action);
}
}

Error handling

All fallible methods return Result<_, FetchError>. Error variants:

VariantCause
FetchError::InvalidUrl(msg)URL could not be parsed
FetchError::BlockedUrl(url)URL matched a blocked pattern or is a private network address
FetchError::Network(msg)HTTP request failed (timeout, DNS, connection refused)
FetchError::HttpError(status)Non-2xx HTTP status code
FetchError::ResponseTooLarge(size, max)Response exceeded max_response_bytes
FetchError::ActionError(msg)Invalid interaction (element not found, wrong element type, no page loaded)