Skip to content

About

Go SDK for Solari Browser (managed stealth Chromium over CDP/Playwright).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

40 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

solari-browser-go

Go language binding for the Solari Browser platform: create, inspect, and release managed remote-browser sessions, and manage the stored profiles they attach. It behaves identically on the wire to the reference TypeScript SDK (@solarisdk/browser, sdk/src/index.ts).

go get github.com/solari-sdk/solari-browser-go

Module path: github.com/solari-sdk/solari-browser-go, package solari. Requires Go 1.23+ and github.com/chromedp/chromedp.

Feature coverage across all five languages is on the SDK feature matrix.

Not to be confused with github.com/solari-sdk/solari-sandbox-go — that is the desktop/sandbox SDK (microVMs, commands, files, code). This one is the browser platform.

Scope: control plane + browser attachment

This SDK is the REST control plane plus two levels of browser attachment. The TypeScript SDK's launch() returns a live Playwright Browser — Playwright has no Go client, so there is no port of that object model, and there won't be one.

Instead, Sessions.Create hands back the session's raw CDP endpoint, and you drive the browser with chromedp, either directly via Connect, or in one call via Sessions.Launch (create, connect, seed an attached profile's cookies, health-probe, retry, release on Close — added 2026-10-06, previously missing here while TS/Python had it). Both endpoints are returned exactly as the gateway issued them — unlike the Node SDK, nothing is rewritten to a loopback proxy.

Field Use
Session.CDPEndpoint What you want. Raw CDP — Connect, or chromedp / any CDP client.
Session.WSEndpoint Playwright wire protocol. No Go client exists; returned for completeness.

Design

  • Context-first. Every network method takes ctx context.Context first.
  • Errors as typed values. Every failure is a *SolariError carrying Status (HTTP status, or 0 for transport errors) and Code (the gateway's error code). Match with errors.As, or the IsCode shorthand.
  • Nothing is read from the environment. Pass the API key and any overrides explicitly to NewClient.
  • Retries are conservative. Each request is tried up to MaxAttempts times (default 2) with a fixed BackoffMs delay (default 500ms). Only transport errors and HTTP 502/503/504 are retried — notably not 429, where retrying cannot help.

Example: create a session → drive it → release

package main

import (
	"context"
	"log"

	"github.com/chromedp/chromedp"
	solari "github.com/solari-sdk/solari-browser-go"
)

func main() {
	ctx := context.Background()

	client, err := solari.NewClient(solari.ClientOptions{
		APIKey: "slr_live_<id>_<secret>",
	})
	if err != nil {
		log.Fatal(err)
	}

	session, err := client.Sessions.Create(ctx, solari.CreateSessionOptions{
		Stealth: true,
		Proxy:   solari.ProxyCountry("us"),
	})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Sessions.Release(context.Background(), session.ID)

	browserCtx, cancel, err := solari.Connect(ctx, session)
	if err != nil {
		log.Fatal(err)
	}
	defer cancel()

	var title string
	if err := chromedp.Run(browserCtx,
		chromedp.Navigate("https://example.com"),
		chromedp.Title(&title),
	); err != nil {
		log.Fatal(err)
	}
	log.Println("title:", title)
}

Connect dials nothing itself — the connection is established on the first chromedp.Run, and failures surface there. cancel() detaches the client; it does not release the session, which keeps running until Sessions.Release or its ExpiresAt deadline.

One-call Launch

Create + connect + seed + probe + retry + release, in one call:

launched, err := client.Sessions.Launch(ctx, solari.LaunchOptions{
	Create:  solari.CreateSessionOptions{Stealth: true},
	Retries: 2,
})
if err != nil {
	log.Fatal(err)
}
defer launched.Close(context.Background())

var title string
if err := chromedp.Run(launched.Browser, chromedp.Navigate("https://example.com"), chromedp.Title(&title)); err != nil {
	log.Fatal(err)
}
log.Println("title:", title)

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, chromedp 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 MaxAttempts on ClientOptions above) — on a transient failure the dead session is released and a fresh one created. Unlike the TS/Rust handles, there is no implicit release on GC — Close (or a defer) is required for a prompt release, or the session lingers until its plan-tier expiry.

Sessions

session, err := client.Sessions.Create(ctx, solari.CreateSessionOptions{
	ProfileID:  "prof_1",   // attach a stored profile
	Recording:  true,       // record the session for replay
	Stealth:    true,       // runtime stealth shim
	Captcha:    true,       // managed captcha solving (requires Stealth)
	Proxy:      solari.ProxySmart,

	SessionTimeoutMs: 600000, // end earlier than the plan maximum, in ms (at least 60000)
	KeepAlive:      true,   // hold the session if the client drops, until its deadline or Release
	UserMetadata:   map[string]interface{}{"job": "nightly-42"}, // stored with the session
})

view, err := client.Sessions.Get(ctx, session.ID)          // current view
page, err := client.Sessions.List(ctx, solari.ListSessionsOptions{
	Limit:    50,
	Metadata: map[string]string{"job": "nightly-42"}, // optional: match userMetadata
})                                                          // page.Sessions, page.NextCursor
err = client.Sessions.Release(ctx, session.ID)             // idempotent; waits for the ack
err = client.Sessions.ReleaseAndWait(ctx, session.ID)      // same call, named like TS releaseAndWait / Python release_and_wait
link, err := client.Sessions.ReplayURL(ctx, session.ID)    // presigned replay link
raw, err := client.Sessions.DownloadReplay(ctx, session.ID) // replay bytes (NDJSON)

All durations are milliseconds. SessionTimeoutMs is rounded up to whole seconds for the gateway (1001 ms is 2 s) and the gateway refuses anything under 60 s; a negative or too-large value is an error and zero leaves the plan maximum. Launch's probe bound is LaunchOptions.ProbeTimeoutMs.

Deprecated: CreateSessionOptions.TimeoutSeconds and LaunchOptions.ProbeTimeout still work; use SessionTimeoutMs and ProbeTimeoutMs. When both are set the new one wins. They 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 endpoints. Pass page.NextCursor as ListSessionsOptions.Cursor for the next page; it is empty on the last one.

The zero CreateSessionOptions is valid and requests a plain session. Release and Profiles.Delete tolerate a 404, so they are safe to call twice.

Replays are available ~1-3s after Release, and only for sessions created with Recording: true. DownloadReplay returns the bytes exactly as stored — gzipped by default, per ReplayURL.ContentEncoding.

Proxy egress

CreateSessionOptions.Proxy takes either a preset or a full request. Managed egress requires Stealth: true.

Proxy: solari.ProxyCountry("gb")  // country shorthand
Proxy: solari.ProxySmart          // gateway picks + escalates per host
Proxy: solari.ProxyOff            // no managed egress

Proxy: solari.ProxyRequest{
	Country:         "us",
	Tier:            solari.TierMobile, // or TierResidential (default) / TierStatic
	ASN:             "21928",           // pin the carrier (T-Mobile)
	Session:         "warm-1",          // sticky egress IP
	SessionDuration: 10,                // minutes (1-30)
	State:           "california",      // US-only
	City:            "los_angeles",     // US-only
}

The resolved egress comes back on Session.Proxy (*ResolvedProxyConfig), and is nil when no managed proxy was requested. To check what a gateway supports before asking:

countries, err := client.Proxy.Countries(ctx)
// countries.Enabled, countries.Countries → ["us", "gb", …]

Profiles

A profile is a persisted cookie jar + localStorage — the same shape as Playwright's storageState.

prof, err := client.Profiles.Create(ctx, "shopper")
profiles, err := client.Profiles.List(ctx)
res, err := client.Profiles.Save(ctx, prof.ID, state) // → res.Version, res.SizeBytes
err = client.Profiles.Delete(ctx, prof.ID)            // idempotent

Editing a profile

Profiles.Edit opens the profile editor on an existing profile and returns an *Editor. Send the person to ed.URL; it is valid for about 30 minutes (ed.ExpiresAt). There is no auto-save and no save without closing.

ed, err := client.Profiles.Edit(ctx, prof.ID, "refresh login") // reason is optional ("")
fmt.Println(ed.URL, ed.ExpiresAt)

st, err := ed.Status(ctx)                    // st.EditorStatus: idle|launching|editing|saving|error
version, err := ed.SaveAndClose(ctx)         // store the state and close the editor
err = ed.Exit(ctx)                           // close without saving
version, err = ed.WaitForSave(ctx, 10*time.Minute) // poll until someone saves from the page

An expired or used link fails with a *SolariError of status 404, an editor that is not running with 409, and a failed save with 502 (the editor goes to error: Exit and start a new Edit; a second save gets 409). Save and exit are never retried automatically. WaitForSave fails with status 408 on timeout or link expiry.

Attaching a profile populates the session's storage state. Because Go cannot express TypeScript's undefined vs null, the tri-state is carried by two fields:

StorageStateAttached StorageState Meaning
false nil No profile was attached.
true nil The profile exists but is empty.
true non-nil The profile's contents.

StorageState preserves top-level keys it does not model (in Extra), so a Sessions.Create → Profiles.Save round trip never silently drops fields.

Errors

session, err := client.Sessions.Create(ctx, solari.CreateSessionOptions{Stealth: true})
if err != nil {
	var serr *solari.SolariError
	if errors.As(err, &serr) {
		log.Printf("status=%d code=%s: %s", serr.Status, serr.Code, serr.Message)
	}
	// or, more directly:
	if solari.IsCode(err, solari.CodeConcurrencyLimitExceeded) {
		// at the org's live-session cap — back off and retry later
	}
}

Known codes: CodeFeatureRequiresPlan, CodeConcurrencyLimitExceeded, CodePlanLimitExceeded, CodeBrowserUnhealthy. The gateway may add more, so compare rather than switching exhaustively.

Client options

Option Default Notes
APIKey — Required. Format slr_live_<id>_<secret>.
Region RegionUSWest More regions coming soon. Ignored when BaseURL is set.
BaseURL region URL Override for staging / self-hosted gateways.
HTTPClient — Overrides the default client; TimeoutMs is then ignored.
MaxAttempts 2 Tries per request, counting the first.
BackoffMs 500 Fixed delay between attempts. Pointer, so 0 is distinguishable from unset.
TimeoutMs 90000 Per-attempt bound.

Client is safe for concurrent use.

Tests

The suite is fully offline — every case runs against httptest servers and asserts request shapes and response parsing. No API key, gateway, or browser needed.

go build ./... && go test ./...

About

Go SDK for Solari Browser (managed stealth Chromium over CDP/Playwright).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages