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.)
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
🚨 CORRECTED 2026-10-04 — path = "sdk/rust" is the in-tree dev-source
path from the monorepo (pinetree-browser/sdk/rust), not an install for
this published crate. Published on crates.io as solari-browser (currently
0.2.2).
Control plane only (dependency-light — reqwest, serde, thiserror, url):
[]
= "0.2"
= { = "1", = ["macros", "rt-multi-thread"] }
With browser automation (pulls in chromiumoxide):
[]
= { = "0.2", = ["connect"] }
~~```toml [dependencies] solari-browser = { path = "sdk/rust" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
With browser automation (pulls in `chromiumoxide`):
```toml
[dependencies]
solari-browser = { path = "sdk/rust", features = ["connect"] }
```~~
## Usage
```rust
use solari_browser::{Client, ClientOptions, CreateSessionOptions, ProxySpec};
#[tokio::main]
async fn main() -> Result<(), solari_browser::SolariError> {
// Default region "us-west" → https://api.getsolari.com
let client = Client::new(ClientOptions::default_region("slr_live_…"))?;
// Or point at staging / a self-hosted gateway:
// Client::new(ClientOptions::new("slr_live_…", "https://gw.example.com"))?
// There is no env-var fallback — the key and URL are always explicit.
let session = client
.sessions()
.create(
CreateSessionOptions::new()
.stealth(true)
.proxy(ProxySpec::country("us")),
)
.await?;
println!("cdp {}", session.cdp_endpoint);
println!("expires {}", session.expires_at);
client.sessions().release(&session.id).await?;
Ok(())
}
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?
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.
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.