#headless-browser #cdp #headless #browser #automation #dom

oxibrowser-webapi

DOM and WebAPI implementations backed by html5ever

6 releases (breaking)

new 0.9.1 May 16, 2026
0.9.0 May 16, 2026
0.8.0 May 16, 2026
0.7.0 May 16, 2026
0.2.0 May 13, 2026

#1658 in Web programming


Used in 3 crates (2 directly)

MIT license

71KB
1.5K SLoC

🌐 OxiBrowser

The headless browser built in pure Rust for AI agents.

Not a Chromium fork. Not a C++ wrapper. A browser engine written from scratch in Rust, designed from day one for automation, web scraping, and AI-driven workflows.

crates.io docs.rs License: MIT GitHub stars CI Rust

Report Bug Β· Request Feature Β· Read the Docs Β· Discord


21 MB
Single static binary
~50 ms
Cold start time
~8 MB
Base memory
279 tests
Full coverage
Zero C deps
Pure Rust
OxiBrowser Headless Chrome Lightpanda
21 MB binary ~400 MB install ~80 MB binary
~8 MB RAM base ~200 MB RAM base ~30 MB RAM base
~50 ms startup ~800 ms startup ~10 ms startup
Pure Rust (boa) C++ (V8) Zig (V8)
MIT License BSD / ToS AGPL-3.0

✨ Why OxiBrowser?

You're building AI agents that need to browse the web. You don't need a full browser with GPU rendering, audio output, and extension support. You need something fast, small, and programmable.

OxiBrowser is built for exactly that use case:

  • πŸ€– AI-Agent First β€” Native OXI CDP domain with getMarkdown(), getPageInfo(), and text-first rendering
  • ⚑ Blazing Fast β€” Cold starts in ~50ms, no Chromium overhead, no Node.js required
  • πŸ¦€ Pure Rust β€” Zero C dependencies. boa_engine for JS (no V8). Single static binary. Memory-safe.
  • πŸ”Œ CDP Compatible β€” Puppeteer, Playwright, and any Chrome DevTools Protocol client works out of the box
  • πŸ›‘οΈ Secure by Default β€” SSRF protection with CIDR blocking, robots.txt respect, no sandbox escape surface
  • πŸ“¦ Tiny Footprint β€” 21 MB binary, ~8 MB base memory. Run 100 instances without breaking a sweat

πŸš€ Quick Start

Install

Cargo (all platforms)

cargo install oxibrowser

Build from source

git clone https://github.com/a7garden/oxibrowser.git
cd oxibrowser
cargo build --release
# Binary at ./target/release/oxibrowser

Use as a library

# Cargo.toml
[dependencies]
oxibrowser-core = "0.7"

Fetch a page

oxibrowser fetch https://example.com

Start CDP server

oxibrowser serve --port 9222

Then connect with Puppeteer:

import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'ws://127.0.0.1:9222',
});

const page = await browser.newPage();
await page.goto('https://news.ycombinator.com');

// Get markdown β€” OxiBrowser's AI-native feature
const md = await page.evaluate(() => {
  // OXI domain available in evaluate context
});

console.log(await page.title());
await browser.close();

Rust API

use oxibrowser_core::Browser;
use oxibrowser_core::config::BrowserConfig;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let browser = Browser::new(BrowserConfig::default()).await?;
    let session = browser.new_session().await?;
    
    session.navigate("https://example.com").await?;
    
    let title = session.evaluate("document.title").await?;
    println!("Title: {:?}", title);
    
    Ok(())
}

πŸ— Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            Puppeteer / Playwright / Rust CDP          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ CDP WebSocket
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 CDP Server (10 domains)               β”‚
β”‚  Browser Β· DOM Β· Fetch Β· Input Β· Network             β”‚
β”‚  OXI Β· Page Β· Runtime Β· Target                       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚          Browser β†’ Session β†’ Page β†’ Frame            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  WebAPI  β”‚  Network β”‚  JS Runtime  β”‚  CSS Rendering  β”‚
β”‚  DOM     β”‚  HTTP    β”‚  boa_engine  β”‚  PNG screenshot β”‚
β”‚  Tree    β”‚  Cookies β”‚  ES2024+     β”‚  ASCII/Unicode  β”‚
│  Storage │  SSRF    │  persistent  │  text→image     │
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   html5ever Β· encoding_rs Β· reqwest Β· image Β· boa    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Crate Structure

Crate Lines Purpose
oxibrowser 1,233 Binary + CLI (fetch, serve, version)
oxibrowser-core 12,953 Browser engine: Session, Page, Frame, JS Runtime
oxibrowser-cdp 4,392 CDP WebSocket server with 10 domain handlers
oxibrowser-webapi 1,549 DOM tree, CSS selectors, Markdown conversion
Total 20,127

🌟 Features

JavaScript Runtime (ES2024+)

Powered by boa_engine β€” pure Rust, no V8 dependency:

Web API Status
document.querySelector / querySelectorAll βœ… Full
document.createElement / createTextNode βœ… Full
element.appendChild / removeChild / insertBefore βœ… Full
element.getAttribute / setAttribute / removeAttribute βœ… Full
element.cloneNode / remove() βœ… Full
element.style (CSSStyleDeclaration) βœ… Property accessor
element.classList (DOMTokenList) βœ… Property accessor
element.textContent / innerHTML βœ… Read/Write
element.addEventListener / dispatchEvent βœ… Full
element.click() βœ… With event handlers
fetch() βœ… Full (channel bridge)
XMLHttpRequest βœ… Full with callbacks
localStorage βœ… Persistent
MutationObserver βœ… observe/disconnect/takeRecords
setTimeout / setInterval βœ… TokioJobQueue
console.log/warn/error βœ… With formatting
URL / URLSearchParams βœ… Full
crypto.getRandomValues βœ… Pseudo-random
TextEncoder / TextDecoder βœ… UTF-8
atob / btoa βœ… Base64
requestAnimationFrame βœ… Polyfill

CDP Protocol (Chrome DevTools Protocol)

10 domain handlers β€” Puppeteer and Playwright compatible:

Domain Key Methods
Browser getVersion, close
DOM getDocument, describeNode, querySelector, querySelectorAll
Fetch enable/disable, continueRequest, failRequest, fulfillRequest, getResponseBody
Input dispatchKeyEvent, dispatchMouseEvent, insertText
Network enable/disable, setExtraHTTPHeaders, getResponseBody
OXI πŸ€– getMarkdown, getPageInfo β€” AI-native extensions
Page navigate, captureScreenshot, getFrameTree, getTitle
Runtime evaluate, callFunctionOn, enable, consoleAPICalled
Target getTargets, attachToTarget, detachFromTarget

OXI Domain β€” Built for AI Agents

The OXI CDP domain provides AI-optimized APIs that no other browser offers:

import websockets, json, asyncio

async def ai_scrape():
    ws = await websockets.connect('ws://localhost:9222/ws')
    
    # Navigate
    await ws.send(json.dumps({
        "id": 1, "method": "Page.navigate",
        "params": {"url": "https://news.ycombinator.com"}
    }))
    await asyncio.sleep(2)
    
    # Get clean markdown β€” perfect for LLM ingestion
    await ws.send(json.dumps({
        "id": 2, "method": "OXI.getMarkdown"
    }))
    resp = json.loads(await ws.recv())
    print(resp['result']['markdown'])  # Clean markdown output
    
    # Get structured page info
    await ws.send(json.dumps({
        "id": 3, "method": "OXI.getPageInfo"
    }))
    info = json.loads(await ws.recv())
    print(info['result'])  # title, url, status, content-type, etc.

Network Layer

Feature Description
HTTP Client reqwest with cookie persistence, redirect following
Cookie Jar Domain-scoped cookie storage with Set-Cookie parsing
SSRF Protection CIDR blocking for private network ranges
robots.txt RFC 9309 compliant parser, --obey-robots flag
Network Interception Pause, modify, or block any request via Fetch domain
Custom Headers Per-session and per-request header injection
Charset Detection encoding_rs for automatic charset detection and conversion

CSS Text Rendering

  • ASCII/Unicode text output β€” Render DOM to readable text with proper indentation
  • Markdown conversion β€” Full HTMLβ†’Markdown with heading, link, and list support
  • PNG screenshots β€” Built-in 8Γ—16 bitmap font, renders text content as images
  • No external dependencies β€” Font data embedded in binary

πŸ§ͺ Testing

# Run all 279 tests
cargo test --workspace

# E2E CDP tests (23 tests with real WebSocket)
cargo test -p oxibrowser-cdp

# Integration tests (real websites, --ignored)
cargo test --workspace -- --ignored

# Puppeteer smoke tests
cargo test -p oxibrowser --test smoke

πŸ“‹ CLI Reference

oxibrowser 0.7.0
Headless browser with CDP support

USAGE:
    oxibrowser <COMMAND>

COMMANDS:
    fetch     Fetch and render a URL
    serve     Start CDP WebSocket server
    version   Print version information

FETCH OPTIONS:
    <URL>                  URL to fetch
    --dump <FORMAT>        Output format: text, html, markdown [default: text]

SERVE OPTIONS:
    --host <HOST>          Bind address [default: 127.0.0.1]
    --port <PORT>          Bind port [default: 9222]
    --obey-robots          Respect robots.txt
    --log-level <LEVEL>    Log level: trace, debug, info, warn, error [default: info]

πŸ”§ Advanced Usage

Custom Browser Configuration

use oxibrowser_core::{Browser, config::BrowserConfig};
use std::time::Duration;

let config = BrowserConfig {
    user_agent: "MyBot/1.0".to_string(),
    timeout: Duration::from_secs(30),
    obey_robots: true,
    max_redirects: 10,
    ..Default::default()
};

let browser = Browser::new(config).await?;

Request Interception

// With Puppeteer
const client = await page.target().createCDPSession();

await client.send('Fetch.enable', {
    patterns: [{ urlPattern: '*ads*' }]
});

client.on('Fetch.requestPaused', async ({ requestId }) => {
    // Block ad requests
    await client.send('Fetch.failRequest', {
        requestId,
        reason: 'BlockedByClient'
    });
});

Screenshot Capture

// PNG screenshot via CDP
const { data } = await client.send('Page.captureScreenshot', {
    format: 'png'
});
// data is base64-encoded PNG

🀝 Contributing

Contributions are welcome! Whether it's:

  • πŸ› Bug reports β€” Open an issue
  • πŸ’‘ Feature requests β€” Start a discussion
  • πŸ”§ Pull requests β€” Fork, branch, PR. All PRs need passing tests.
  • πŸ“– Documentation β€” Fix typos, add examples, improve guides

Development Setup

git clone https://github.com/a7garden/oxibrowser.git
cd oxibrowser
cargo build
cargo test --workspace
cargo clippy --workspace -- -D warnings

πŸ“„ License

OxiBrowser is licensed under the MIT License.


πŸ™ Acknowledgments

  • Lightpanda β€” Architecture inspiration (Browser β†’ Session β†’ Page β†’ Frame hierarchy)
  • boa_engine β€” Pure Rust JavaScript engine (ES2024+)
  • html5ever β€” HTML parser from the Servo project
  • reqwest β€” Ergonomic HTTP client for Rust
  • tokio β€” Async runtime powering the entire networking stack

⬆ Back to Top

Made with πŸ¦€ in Rust

Dependencies

~6–9.5MB
~100K SLoC