cirrus
An ergonomic Rust HTTP client for the Salesforce REST API.
This project is in no way affiliated with Salesforce.
cirrus is a strongly-typed, async-first client built on reqwest and
tokio. It covers the everyday surface of the Salesforce REST API — CRUD,
SOQL/SOSL, Bulk 2.0, composite, Tooling, Apex REST, Event Monitoring — plus a
small set of cross-cutting niceties (retry/backoff, auto-refresh on 401, a
streaming pagination iterator, conditional-request helpers, structured
tracing events) that you'd otherwise have to assemble by hand.
It also exposes an open-ended escape hatch so any endpoint not yet typed is a one-liner away.
Quick start
cirrus runs on the tokio runtime — internal retry/backoff
sleeps and the auth-cache synchronization both rely on it. Add tokio alongside
cirrus so you get a runtime entrypoint (#[tokio::main]) and a scheduler:
[]
= "0.1"
= { = "1", = ["macros", "rt-multi-thread"] }
use StaticTokenAuth;
use Cirrus;
use Arc;
async
Features
REST surface
- sObject CRUD —
sf.sobject("Account").{create, retrieve, update, delete, upsert}with typed and untyped variants. Multipart blob upload forContentVersion/Document/Attachment. - SOQL —
sf.query(...),sf.query_all(...),sf.query_more(...). Typed variants (query_as::<T>(...)) and afutures::Streampagination iterator (query_stream(...)) that walksnextRecordsUrllocators transparently. - SOSL —
sf.search(...)andsf.parameterized_search(...). - Composite — all four shapes:
composite/batch,composite/tree,composite/sobjects(collections), and the generic chained-reference/compositeendpoint. - Bulk API 2.0 — ingest (
bulk().ingest()) and query (bulk().query()) with the full lifecycle:create,upload,close,abort,get,results(withSforce-Locatorcursor pagination), raw CSV downloads. - Tooling API —
tooling().{describe_global, sobject(name).*, query, search, execute_anonymous}. - Apex REST — thin passthrough for custom
/services/apexrest/...endpoints. - Event Monitoring —
event_monitoring().{download, download_url}for binaryEventLogFileCSV downloads. - Versions, limits, describe —
sf.versions(),sf.limits(),sf.sobjects().describe_global(),sf.sobject(name).describe().
Authentication
Five OAuth 2.0 flows + static-token mode, all behind a common AuthSession
trait so handlers don't care which flow you used.
- JWT Bearer (RFC 7523) —
auth::JwtAuth::builder() - Refresh Token (RFC 6749 §6) —
auth::RefreshTokenAuth::builder() - Client Credentials (RFC 6749 §4.4) —
auth::ClientCredentialsAuth::builder() - Web Server with PKCE (RFC 6749 §4.1 + RFC 7636) —
auth::WebServerFlow::builder() - Token Exchange (RFC 8693) —
auth::TokenExchange::builder(), including hybrid grant - Static token —
auth::StaticTokenAuth::new(token, instance_url)for paste-from-sf-org-displayworkflows
Auto-refresh on 401: when an expired token surfaces, JwtAuth,
RefreshTokenAuth, and ClientCredentialsAuth invalidate their cache and
retry once with a fresh token, transparently. Compare-and-swap semantics avoid
clobbering a token a concurrent task just refreshed.
Cross-cutting
- Retry + backoff —
RetryPolicycovers 429, 503, and transient 5xx with full jitter; honorsRetry-After. Configurable; off by default for non-idempotent 5xx. - Sforce-Limit-Info capture — every response's API quota header is parsed
and surfaced via
sf.last_limit_info(). - Pagination as
futures::Stream— composes withStreamExtfrom any async ecosystem, so the consumer API surface isn't tied to a specific combinator crate. Drop the stream → no further fetches. (Execution still runs on tokio, like the rest of the crate.) - Conditional requests —
describe_*_if_modified_since(SystemTime)returnsOption<T>;Noneon 304 Not Modified. - Structured
tracingevents —cirrus::retry,cirrus::auth,cirrus::limit_infotargets. Never logs tokens or bodies.
The escape hatch
The Salesforce REST surface is too large to wrap every endpoint. The client
exposes verb methods that handle path resolution, auth, retry, and the
Sforce-Limit-Info capture for any endpoint:
sf..await?; // versioned
sf..await?; // instance-rooted
sf..await?; // fully-qualified
Three-mode path resolution: relative → /services/data/{version}/...,
leading-/ → instance-rooted, http(s):// → passthrough.
For unusual cases (custom headers, binary download, SSE), request_builder and
execute give you a pre-authenticated reqwest::RequestBuilder and full bypass
respectively.
Examples
See the examples/ directory:
simple_query.rs— connect, run a SOQL query, print resultscrud_cycle.rs— full create → retrieve → update → delete on anAccountbulk_ingest.rs— Bulk API 2.0 CSV ingeststreaming_pagination.rs— iterate all records viaquery_streamapex_passthrough.rs— call a custom Apex REST endpoint
Run an example after setting the required env vars (any sandbox / Developer Edition / scratch org):
# from `sf org display`
Testing
Integration tests against a real org are gated behind #[ignore] and an
opt-in env-var:
The integration harness refuses to run unless the configured instance URL
matches a known sandbox / Developer Edition / scratch My Domain pattern. See
tests/integration/common.rs for details.
License
Licensed under the MIT license.