Solari

Errors

How both gateways report failure on the wire: the body shape, every status they return, every machine-readable code, and which failures are worth retrying. This page is the HTTP contract; no SDK code appears on it. For the client-side error types that wrap these responses, see the SDK reference.

Contents

Error body

Every error from either gateway is a JSON object with a human-readable error. Some handlers add a machine-readable code. Branch on the status or on code, never on the prose. The prose is not stable across releases.

FieldTypeDescription
errorstringHuman-readable message. Always present. Not stable, so do not parse it.
codestringMachine-readable discriminator, when the handler sets one. See code values.
detailstringBrowser gateway only. Upstream diagnostic, truncated to 512 characters. Prose. Do not branch on it.
messagestringVM gateway only. Alternate message field some clients read.
retryablebooleanVM gateway only. true marks a transient failure an idempotent client should retry with backoff. Prefer this over the bare status.

Individual errors carry extra context alongside these: plan, cap, feature, limit, supported, profileId. They are documented on the endpoints that emit them in the API reference. The object is open: treat unknown fields as additive.

{
  "code": "ConcurrencyLimitExceeded",
  "error": "Concurrent session limit reached (20)",
  "plan": "starter",
  "cap": 20
}

Status codes

Successes are 200, except 201 on create, 202 on an async template build, 204 on browser release, and 101 on a WebSocket upgrade. A VM exec whose command exits non-zero is still 200. The call succeeded, the command failed. Read exitCode.

StatusMeaningRetry?
400Malformed body, or a cross-field rule was broken. proxy and captcha require stealth: true on the browser gateway; the VM gateway rejects bad template and recording combinations here.No
401No Authorization header, a non-Bearer scheme, or a key that failed verification.No
402The plan lacks a requested feature (FeatureRequiresPlan), or the prepaid balance is exhausted (InsufficientCredit, VM).No
403Profile cap reached (PlanLimitExceeded, browser) or the plan excludes VMs (NotEntitled).No
404Deliberately opaque. An unknown ID, a forged signature, and another org's valid ID are indistinguishable by design, so existence cannot be probed.No
409Resource-state conflict. The profile editor is open or a version clashed (browser); a template is still building or a snapshot still has live children (VM).No
413Browser gateway only. Content-Length exceeded the route cap (16 KB by default, 1 MB on POST /profiles/:id/save). Enforced from the header before the body is parsed; limit echoes the cap in bytes.No
429ConcurrencyLimitExceeded. The org is at its concurrent-session cap. The cap is per plan — see Plans & pricing. Free is 3 browsers and 1 sandbox.No, see retry semantics
500Unexpected failure. The VM gateway defensively tears down any already-live VM, undoes partial writes, and frees the reservation before returning this.Maybe
501VM gateway only. The feature is not configured on this deployment: every volumes route returns 501 when VOLUMES_TABLE is unconfigured, and port preview returns it with no previewDomain. A configuration fact, not a transient one.No
502An upstream was unreachable or errored: the control plane (browser), or a host that rejected an assign, restore, or pause (VM).Yes
503Capacity or a dependency. No pool or host had free capacity; the concurrency store is wedged (ConcurrencyCheckUnavailable, browser); or admission is unreachable (VM). The last two fail closed rather than let an org slip past its cap or balance gate.Yes
504Not emitted by either gateway. It comes from the load balancer in front of them when a request outlives its timeout. The work may still have completed.Yes, if idempotent

Code values

The complete set. Fourteen values across the two gateways; a handler that does not set one leaves code absent, so always tolerate its absence.

codeStatusGatewayMeaning
FeatureRequiresPlan402BothThe plan lacks a requested feature: stealth, proxy, captcha, desktops, custom templates. Carries feature and plan.
ConcurrencyLimitExceeded429BothAt the concurrent-session cap. Carries plan and cap. Not retryable.
PlanLimitExceeded403BrowserProfile cap reached. Generated by the control plane and forwarded verbatim; the gateway never mints it.
ConcurrencyCheckUnavailable503BrowserThe concurrency store is wedged. The gateway fails closed rather than let an org blow past its cap. Retryable.
InvalidSessionId404BrowserThe session ID was malformed, forged, or another org's. Nothing was released. Do not treat as success.
NotEntitled403VMThe plan does not include desktops or sandboxes.
InsufficientCredit402VMAdmission rejected on prepaid balance.
TemplateKindMismatch400VMThe custom template's kind does not match the requested kind.
TemplateNotReady409VMThe template exists but is still building, or failed.
TemplateBuilding409VMA template cannot be deleted while its build is running.
SnapshotHasChildren409VMLive VMs still descend from this snapshot.
LocalFilesUnsupported400VMcompiled.localFiles is not implemented. Fetch files inside the build instead.
RecordingRequiresDesktop400VMrecord: true on a headless kind: "sandbox". There is no display to capture.
RecordingRequiresGoldenBoot400VMrecord: true combined with fromSnapshot or a tpl_… template. The host cannot wire recording on the restore path.
BrowserUnhealthy is not a wire code

The TypeScript SDK's error union names BrowserUnhealthy, but no gateway response ever carries it. The client synthesizes it when its own post-connect probe fails or times out. Do not write an HTTP handler expecting it. See the browser SDK reference for the client-side union.

Retry semantics

502, 503, 504 and transport errors (connection reset, DNS failure, timeout) are the retryable class: an upstream was unreachable or capacity was briefly unavailable, and the same request may well succeed a moment later. Back off with jitter. Everything in the 4xx range is deterministic. The same request will fail the same way until you change the request, the plan, or the resource state.

429 is not retryable, and no SDK retries it

ConcurrencyLimitExceeded means your org is already at its concurrent-session cap. Retrying cannot help: a slot only frees when you pause or kill a session. Neither the browser client nor the VM client retries a 429. It propagates to your caller on the first response, and backing off is your job. A tight retry loop here burns quota against a wall.

On the VM gateway a transient failure also carries "retryable": true in the body. Prefer that signal over the bare status. Send Idempotency-Key: <uuid-v4> on create routes to make the retry safe. Without one, a retried create can double-charge you a VM. GET and DELETE are idempotent by nature: deleting an already-gone VM still returns 200 {"ok": true}.

What the SDKs do

The two clients do not share a retry policy. If you are writing against the wire directly, this is the behaviour you are replacing.

BehaviourBrowser clientVM client
Retries which statuses502, 503, 504 only. A 500 or 501 is surfaced immediatelyAny 5xx, or any body with retryable: true
Retries which requestsAll of them, including non-idempotent createsIdempotent only: GET, DELETE, or a request carrying an Idempotency-Key
Retries a 429NoNo
AttemptsmaxAttempts: 2 is the total, so the default is one try plus one retrymaxRetries: 5 counts retries on top of the first attempt, so up to six tries
BackoffFixed 500ms, no jitterExponential with jitter, 150ms doubling to an 8s cap
Timeout90s, applied per attempt300s, applied per attempt
Timeouts are per attempt, not per call

Both clients arm a fresh timer on every attempt, so the wall-clock ceiling for a call is roughly attempts × timeout plus backoff, not the timeout you configured. Budget for the total if you are wrapping these calls in a deadline of your own.

Errors on the wire

A bad key on the browser gateway. The body is the minimal envelope: error and nothing else.

curl -sS -i https://api.getsolari.com/profiles \
  -H "Authorization: Bearer slr_live_not_a_real_key"
HTTP/1.1 401 Unauthorized
content-type: application/json

{"error":"Unauthorized"}

A capacity failure on the VM gateway. retryable is the signal to branch on, and this request is worth sending again after a backoff.

{
  "error": "No sandbox host available",
  "retryable": true
}

Volumes on a deployment with no VOLUMES_TABLE. A 501 is a fact about the deployment. Retrying it will return the same thing forever.

{
  "error": "Volumes are not configured"
}

Branch on the status first, then on code. Capturing the body and the status separately keeps both available:

STATUS=$(curl -sS -o /tmp/resp.json -w '%{http_code}' \
  -X POST https://api.getsolari.com/sandboxes \
  -H "Authorization: Bearer $SOLARI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"template":"base","cpu":2,"memMb":2048}')

case "$STATUS" in
  2*)         jq -r .sandboxId /tmp/resp.json ;;
  429)        echo "at cap: pause or kill a session, retrying will NOT help" ;;
  502|503|504) echo "transient: resend with the SAME Idempotency-Key" ;;
  *)          jq -r '"\(.code // "no-code"): \(.error)"' /tmp/resp.json ;;
esac

Traps

  • GET /sessions/:id always 404s. The browser gateway proxies to a pool route that does not exist, so every well-formed request to a live session returns a 404 whose body is the plain-text string 404 Not Found served under a JSON content type. It will not parse as JSON. Do not build against it. See the endpoint's entry.
  • A 404 on release is not a no-op. DELETE /sessions/:id acks 204 even for an already-released session, so a 404 (InvalidSessionId) there means the release did not happen. Do not treat it as success.
  • Not every 5xx is transient. 501 is a configuration fact and 500 is a bug; only 502/503/504 reliably reward a retry.
  • A revoked key can keep working for up to 60 seconds. The browser gateway caches verified bearers, so revocation is not instant unless the control plane invalidates it first.
  • A 200 from exec does not mean your command worked. The HTTP call succeeded; the command's own exitCode is in the body.

For the routes these errors come from, see the API reference. If you hit an error that isn't listed here, or one whose behaviour doesn't match this page, email hello@getsolari.com.