CI codecov Elixir License: MIT

Elixir binding for the obscura headless browser engine, built with Rustler.

Obscurax wraps the full obscura API — browser launch, page navigation, V8 JavaScript evaluation, element operations, request/response observers, and request interception — behind a synchronous-feeling Elixir API. Each page runs on its own OS thread with a dedicated V8 isolate, so multiple pages execute JavaScript in true parallel.

Installation

The package is not on Hex yet. Add it as a git dependency:

def deps do
  [
    {:obscurax, git: "https://github.com/gilbertwong96/obscurax.git", branch: "main"}
  ]
end

Build requirements

  • Rust 1.91+ (rustup) — only needed for :force_build or local development. A rust-toolchain.toml is included so rustup automatically installs the correct version.
  • C++ compiler — clang/gcc on Linux, Xcode Command Line Tools on macOS, MSVC on Windows (required when building from source)
  • git — to fetch obscura as a Cargo dependency

By default, Obscurax downloads a precompiled NIF at compile time — no Rust toolchain required. The precompiled binaries are hosted on GitHub Releases and verified with SHA-256 checksums.

To force building from source instead (e.g. for development or unsupported targets), set OBSCURAX_BUILD=true:

OBSCURAX_BUILD=true mix compile

The first source build takes ~5 minutes because V8 builds from source. Subsequent builds are fast — the V8 artifact is cached by Cargo.

Rustler compiles the Rust crate into a native shared library (obscurax.so on Linux/macOS, obscurax.dll on Windows) and places it in priv/native/.

Quick start

{:ok, browser} = Obscurax.Browser.new()
{:ok, page} = Obscurax.Browser.new_page(browser)
:ok = Obscurax.Page.goto(page, "https://example.com")
{:ok, title} = Obscurax.Page.evaluate(page, "document.title")
IO.puts(title)
Obscurax.Page.close(page)

Bang variants raise Obscurax.Error on failure:

browser = Obscurax.Browser.new!()
page = Obscurax.Browser.new_page!(browser)
Obscurax.Page.goto!(page, "https://example.com")
"Example Domain" = Obscurax.Page.evaluate!(page, "document.title")

Features

  • Full obscura API — Browser, Page, Element operations, CookieStore
  • Synchronous-feeling Elixir API — async navigation and V8 evaluation under the hood, clean {:ok, result} / {:error, %Obscurax.Error{}} returns
  • Dirty CPU scheduler — V8 evaluation runs on dirty CPU schedulers so it never blocks the BEAM
  • Passive observerson_request/2 and on_response/2 fire messages into a callback process for every network event
  • Request interception — pause, inspect, modify, mock, or block requests via enable_interception/1 and reply_intercept/3
  • Stealth mode (opt-in) — Obscurax.Browser.new(stealth: true) to reduce detection

Concurrency

Each Obscurax.Page runs on its own OS thread with a dedicated current_thread tokio runtime and V8 isolate. Pages from the same browser (or different browsers) run in true parallel — V8 evaluation in one page never blocks another:

{:ok, browser} = Obscurax.Browser.new()

urls = ["https://example.com", "https://example.org", "https://example.net"]

tasks =
  for url <- urls do
    Task.async(fn ->
      {:ok, page} = Obscurax.Browser.new_page(browser)
      :ok = Obscurax.Page.goto(page, url)
      {:ok, html} = Obscurax.Page.content(page)
      Obscurax.Page.close(page)
      html
    end)
  end

results = Task.await_many(tasks, 30_000)

License

MIT