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: no launch(), and why
The TypeScript SDK's Solari.launch() returns a live Playwright Browser.
Playwright has no Rust binding, so there is no Rust port of it, and there won't
be one.
Rust drives sessions over the raw CDP endpoint instead, via
chromiumoxide — enable the connect feature and use
solari_browser::connect(). 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?;
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.