solari-browser (Rust)
Rust binding for the Solari Browser platform — the control plane: acquire
and release pooled stealth Chromium sessions, manage stored browser profiles,
pull session replays, and inspect managed-proxy egress. It speaks the same wire
contract as the reference @solarisdk/browser TypeScript package
(sdk/src/index.ts).
reqwest (rustls) for REST, serde for the wire types, thiserror for
SolariError. Optionally chromiumoxide for driving the browser.
The crate is solari-browser; the root module is solari_browser. (Distinct
from the desktop/sandbox crate, which is solari-sdk / solari.)
Feature coverage across all five languages is on the SDK feature matrix.
Scope: browser attachment, not a Playwright port
The TypeScript SDK's Solari.launch() returns a live Playwright Browser.
Playwright has no Rust binding, so there is no port of that object model, and
there won't be one.
Rust drives sessions over the raw CDP endpoint instead, via
chromiumoxide, at two levels — both behind the connect feature:
solari_browser::connect()— attach to a session you already created.Client::launch()— a thin one-call convenience (create, connect, seed an attached profile's cookies, health-probe, retry, release on close) that hands back the samechromiumoxide::Browserto drive. Added 2026-10-06 — previously Go/Rust had no equivalent of TS/Python'slaunch()at all.
Note the distinction on every Session:
| field | protocol | use from Rust |
|---|---|---|
ws_endpoint |
Playwright wire protocol | ❌ chromiumoxide can't speak this |
cdp_endpoint |
raw CDP | ✅ this is the one — Puppeteer-shaped |
One other deliberate divergence: the TS SDK proxies both endpoints through a
Node-side loopback LocalProxy. This crate returns the upstream gateway
endpoints verbatim — there is nothing to route through.
Install
Published on crates.io as solari-browser.
Control plane only (dependency-light — reqwest, serde, thiserror, url):
[]
= "0.6"
= { = "1", = ["macros", "rt-multi-thread"] }
With browser automation (pulls in chromiumoxide):
[]
= { = "0.6", = ["connect"] }
Usage
use ;
async
Driving the browser (feature connect)
use CreateSessionOptions;
let session = client
.sessions
.create
.await?;
// Attaches to session.cdp_endpoint; returns the chromiumoxide Browser plus the
// spawned task pumping its event handler (every call resolves through it).
let connected = connect.await?;
let page = connected.browser.new_page.await.unwrap;
println!;
connected.disconnect.await; // drops the browser, awaits the handler
client.sessions.release.await?;
One-call launch (feature connect)
Create + connect + seed + probe + retry + release, in one call:
use ;
let launched = client
.launch
.await?;
let page = launched.browser.new_page.await.unwrap;
println!;
launched.close.await?; // disconnects and releases the session
An attached profile's cookies are seeded into the live browser automatically;
localStorage is not (Playwright restores it for free on the TS/Python path,
chromiumoxide has no equivalent) — seed it yourself against launched.browser if
you need it. retries controls the whole create+connect+probe sequence, not
individual HTTP requests (that's the max_attempts client setting above) — on a
transient failure the dead session is released and a fresh one created.
Profiles
use ;
let profile = client.profiles.create.await?;
// Attach it — the session's storage_state is fetched from its presigned URL.
let session = client
.sessions
.create
.await?;
match &session.storage_state
// …drive the browser, then persist what it accumulated.
client.profiles.save.await?;
for p in client.profiles.list.await?
Editing a profile
profiles().edit opens the profile editor on an existing profile and returns a
ProfileEditor. Send the person to editor.url; it is valid for about 30
minutes (editor.expires_at). There is no auto-save and no save without closing.
let editor = client.profiles.edit.await?; // reason is optional
println!;
let st = editor.status.await?; // st.editor_status: Idle | Launching | Editing | Saving | Error
let version = editor.save_and_close.await?; // store the state and close the editor
editor.exit.await?; // close without saving
let version = editor.wait_for_save.await?; // poll until someone saves from the page
Errors are SolariError::Api: an expired or used link has status 404, an editor
that is not running 409, a failed save 502 (the editor goes to error: exit() and start a new edit; a second save gets 409). Save and exit are never retried automatically.
wait_for_save fails with status 408 on timeout or link expiry.
storage_state is three-state, mirroring the TS storageState?: StorageState | null:
None = no profile attached, Some(None) = profile attached but empty,
Some(Some(_)) = the profile's state.
Managed proxy
use ;
country; // proxy: "gb"
smart; // gateway sweeps on block
off; // explicitly disabled
from;
Managed proxy requires stealth(true). client.proxy().countries() lists the
supported egress countries and reports whether the gateway has proxy credentials
configured at all — this endpoint has no TypeScript equivalent.
Timeout, keep-alive, metadata, get and list
let mut meta = new;
meta.insert;
let session = client.sessions.create.await?;
let view = client.sessions.get.await?; // status, kind, user_metadata, endpoints
let mut opts = default;
opts.limit = Some;
opts.metadata.insert; // optional: match user_metadata
let page = client.sessions.list.await?; // page.sessions, page.next_cursor
All durations are milliseconds. session_timeout_ms is rounded up to whole seconds
for the gateway (1001 ms is 2 s) and the gateway refuses anything under 60 s; zero or
unset leaves the plan maximum. LaunchOptions::probe_timeout_ms bounds the launch probe.
Deprecated: CreateSessionOptions::timeout_seconds (field and builder method)
still works; use session_timeout_ms. When both are set the new one wins. It will not
be removed before the next major release.
list never returns the websocket endpoints (they are bearer capabilities): call
get for a single session's. Pass page.next_cursor as cursor for the next
page; it is None on the last one.
Replays
client.sessions.release.await?; // replays land ~1–3s after release
let replay = client.sessions.replay_url.await?;
println!;
let bytes = client.sessions.download_replay.await?; // NDJSON, gzip by default
Errors
Every fallible method returns Result<T, SolariError>:
| variant | when |
|---|---|
Api |
non-2xx from the API — carries status + parsed code |
Transport |
network failure, or retries exhausted |
Protocol |
a 2xx body that was malformed or missing required fields |
Config |
invalid client options (empty api key, bad base URL) |
Connect |
CDP attach failed (feature connect) |
err.status() and err.code() work on any error. code() yields the typed
SolariErrorCode — FeatureRequiresPlan, ConcurrencyLimitExceeded,
PlanLimitExceeded, BrowserUnhealthy — with unknown wire codes preserved as
Other(String) rather than dropped.
use SolariErrorCode;
match err.code
Retries
Defaults: max_attempts = 2, backoff_ms = 500 (a fixed delay, not
exponential), timeout_ms = 90_000. Only 502 / 503 / 504 and transport
errors are retried — every other status, including 4xx and 500, returns on the
first attempt. Tune per-client:
use ;
let client = new?;
Debug for Client and ClientOptions is hand-written to redact the API key —
neither will print your credentials.
Testing
Fully offline — the API is mocked with an in-process TCP HTTP server; no live gateway is needed.
cargo build
cargo test
The connect feature is off by default, so the core build never pulls in
chromiumoxide. To verify it too: cargo build --features connect.