Rust SDK
The faber-sdk crate is an async client for the Faber API. It covers every
operation, has typed models for the whole contract, retries transient
failures, and ships five runnable examples.
- Built on
reqwestwith rustls (no OpenSSL) andtokio. - Unknown enum values decode into an
Other(String)variant and unknown JSON fields are ignored, so a newer server does not break an older client. - Minimum Rust version: 1.85.
Install
Section titled “Install”[dependencies]faber-sdk = "0.3"tokio = { version = "1", features = ["macros", "rt-multi-thread"] }The library is imported as faber_sdk.
Quick start
Section titled “Quick start”use faber_sdk::{Client, ExecuteRequest, SandboxProfile, Task};
#[tokio::main]async fn main() -> faber_sdk::Result<()> { let client = Client::new("http://localhost:3000", "my-api-key")?;
let request = ExecuteRequest::new() .stop_on_failure(true) .task( Task::new("gcc") .args(["-O2", "-o", "main", "main.c"]) .file("main.c", "#include <stdio.h>\nint main(void){puts(\"hi\");}\n") .cache(true), ) .task(Task::new("./main").sandbox_profile(SandboxProfile::NativeV2));
let response = client.execute(&request).await?; match response.first_failure() { None => print!("{}", response.steps[1].tasks()[0].stdout()), Some(failure) => eprintln!("failed: {} {}", failure.outcome(), failure.stderr()), } Ok(())}Configure
Section titled “Configure”use std::time::Duration;use faber_sdk::{Client, RetryPolicy};
let client = Client::builder("https://faber.example.com") .api_key("my-api-key") .timeout(Duration::from_secs(30)) .retry(RetryPolicy { max_retries: 4, ..RetryPolicy::default() }) .header("x-tenant", "acme") .build()?;| Builder method | Default | Meaning |
|---|---|---|
Client::builder(url) | Service root or API root; /api/v1 is added when missing. A trailing slash is fine. | |
.api_key(key) | none | Sent as Authorization: Bearer <key>. Every route except /health needs it. |
.timeout(duration) / .no_timeout() | 60 s | Per attempt, from connecting until the response body is read. |
.retry(RetryPolicy) | 2 retries, 250 ms, 10 s | See Retries. |
.header(name, value) | Extra header on every request. Invalid names or values fail build(). | |
.http_client(reqwest::Client) | rustls client | Your own client (shared pool, proxy, custom TLS). faber_sdk::reqwest re-exports the matching version. |
Shortcuts: Client::new(url, api_key) and Client::from_env(), which reads
FABER_URL and, if set, FABER_API_KEY. Client is cheap to clone; clones
share one connection pool.
Retries
Section titled “Retries”A request is retried when Faber answers 503 Service Unavailable
(capacity_exhausted, shutting_down) or 429 Too Many Requests
(rate_limited), and when the connection cannot be established. The wait is
the Retry-After header when present, otherwise initial_backoff doubled
per retry with jitter; both are capped at max_backoff. Other 5xx
responses, every other 4xx response and timeouts are never retried. Uploads from a stream or reader are sent once.
use std::time::Duration;use faber_sdk::RetryPolicy;
let patient = RetryPolicy { max_retries: 5, initial_backoff: Duration::from_millis(500), max_backoff: Duration::from_secs(30),};let never = RetryPolicy::none();Operations
Section titled “Operations”| Method | Route | Returns |
|---|---|---|
execute(&ExecuteRequest) | POST /execute | ExecuteResponse |
run(Task) | POST /execute | the TaskResult of a one-task request |
health() | GET /health | HealthResponse |
info() | GET /info | ServiceInfo |
upload_file(FileUpload) | POST /file | UploadResponse |
list_files() | GET /file | Vec<FileInfo> |
download_file(id) | GET /file/{id} | bytes::Bytes |
download_file_to(id, &mut writer) | GET /file/{id} | bytes written (u64) |
delete_file(id) | DELETE /file/{id} | () |
clear_cache() | DELETE /cache | CacheClearResponse |
metrics() | GET /metrics | Prometheus text (String) |
Execute
Section titled “Execute”let response = client.execute(&request).await?;println!("cache: enabled {}, hits {}, stored {}", response.cache.enabled, response.cache.hits, response.cache.stored);
// One task, one result:let result = client.run(Task::new("echo").arg("hello")).await?;assert_eq!(result.stdout(), "hello\n");Task failures (non-zero exit, timeout, OOM, policy violation) are part of the response. Only request-level problems are errors.
Health and info
Section titled “Health and info”let health = client.health().await?;if !health.is_ok() { // 503 while draining is returned here, not as an error, and not retried.}
let info = client.info().await?;println!("{} on {}, default profile {}", info.version, info.arch, info.sandbox_profiles.default);println!("memory limit {} bytes, max {} parallel tasks", info.limits.memory_max_bytes, info.limits.max_parallel_tasks);for toolchain in &info.toolchains { println!("{} {:?}", toolchain.id, toolchain.version);}use faber_sdk::FileUpload;
let uploaded = client .upload_file(FileUpload::bytes("1 2\n").filename("input.txt").content_type("text/plain")) .await?;client.upload_file(FileUpload::path("tests/big-input.txt")).await?; // streamed from diskclient.upload_file(FileUpload::reader(tokio::io::stdin())).await?; // any AsyncReadclient.upload_file(FileUpload::stream(response.bytes_stream())).await?; // any TryStream of bytes
let files = client.list_files().await?; // id, filename, content_type, size, created_atlet bytes = client.download_file(&uploaded.file_id).await?;let mut out = tokio::fs::File::create("copy.txt").await?;client.download_file_to(&uploaded.file_id, &mut out).await?;client.delete_file(&uploaded.file_id).await?;Uploading the same bytes again returns the same file_id with
already_exists: true. File ids that are not alphanumeric are rejected with
Error::InvalidArgument before anything is sent.
Cache and metrics
Section titled “Cache and metrics”let cleared = client.clear_cache().await?; // Err with code cache_disabled if the server has no cachelet text = client.metrics().await?;Building requests
Section titled “Building requests”ExecuteRequest and Task have chainable methods. Every model also has
public fields, so struct literals with ..Default::default() work.
use faber_sdk::{ExecuteRequest, FileInput, SandboxProfile, Task};
let request = ExecuteRequest::new() .cache(false) .stop_on_failure(true) .task( Task::new("gcc") .args(["-O2", "-o", "main", "main.c"]) .file("main.c", source) // UTF-8 text .bytes_file("data/blob.bin", [0_u8, 1, 2]) // base64 on the wire .file("run.sh", FileInput::text(script).executable()) // mode 0755 .file_ref("input.txt", &uploaded.file_id) // uploaded earlier .cache(true), ) .parallel([ Task::new("./main").stdin("1 2\n").sandbox_profile(SandboxProfile::NativeV2), Task::new("./main").stdin("40 2\n").env("LANG", "C"), ]) .task(Task::new("sh").args(["-c", "ls"]).working_dir("/faber/data"));ExecuteRequest | Effect |
|---|---|
.task(Task) | Appends a single-task step. |
.parallel(tasks) | Appends a step whose tasks run concurrently. |
.step(impl Into<Step>) | Appends a Task, Vec<Task> or [Task; N]. |
.cache(bool) | Default for tasks that do not set cache. |
.stop_on_failure(bool) | After a failing task, later steps are reported as not_started. |
Task | Effect |
|---|---|
Task::new(cmd), .arg(a), .args(iter) | Program and arguments. |
.env(k, v), .envs(iter) | Environment variables. |
.stdin(text) | Standard input. |
.file(path, impl Into<FileInput>) | A file; strings become UTF-8 text files. |
.bytes_file(path, bytes) | A binary file. |
.file_ref(path, file_id) | An uploaded file. |
.working_dir(dir) | Start directory (default /faber). |
.sandbox_profile(profile) | CompileV2, NativeV2, CompileV1, NativeV1. |
.cache(bool) | Marks the task deterministic for the step cache. |
FileInput has the constructors text, bytes and file_id, and
.executable() sets mode 0755 on any of them.
Results
Section titled “Results”response.all_succeeded(); // every task ran and exited 0response.first_failure(); // Option<&TaskResult>response.tasks(); // iterator over every task, parallel steps flattened
let step = &response.steps[1]; // StepResult::Task or StepResult::Parallelstep.is_parallel();step.succeeded();let result = &step.tasks()[0]; // tasks() is a slice for both shapes
result.succeeded(); // ran and exited 0result.exit_code(); // Option<i32>; None when the task never ranresult.stdout(); // "" for a task that never ranresult.stderr();result.error(); // Option<&str>: why the task did not runresult.outcome(); // &TaskOutcomeresult.stats(); // memory, CPU, pids, time, truncation, signalresult.is_cached(); // replayed from the step cacheTaskResult is an enum of Completed(CompletedTaskResult) and
Failed(FailedTaskResult) if you prefer to match. TaskOutcome is one of
Exited, Signaled, TimedOut, OutOfMemory, AncestorOutOfMemory,
PidsLimit, OutputLimit, PolicyViolation, NotStarted,
InfrastructureFailure, or Other(String).
Errors
Section titled “Errors”Every operation returns faber_sdk::Result<T> with one Error enum:
| Variant | When |
|---|---|
Error::Api(ApiError) | Faber answered with an error status. |
Error::Http(reqwest::Error) | Connection, timeout, TLS or body-read failure. |
Error::Decode { source, request_id } | A success response did not have the expected shape. |
Error::Io(std::io::Error) | Reading a file to upload or writing a download failed. |
Error::Config(String) | Invalid base URL, API key or header. |
Error::InvalidArgument(String) | Malformed file id or content type. |
ApiError has status, code: Option<ErrorCode> (None when the body was
not a Faber error body, e.g. from a proxy), message, request_id and
retry_after. The accessors error.status(), error.code() and
error.request_id() work on any variant.
use faber_sdk::{Error, ErrorCode};
match client.execute(&request).await { Ok(response) => { /* inspect task results */ } Err(error) if error.code() == Some(&ErrorCode::FileNotFound) => { /* upload, then retry */ } Err(Error::Api(api)) => eprintln!("{} {:?}: {} [request {:?}]", api.status, api.code, api.message, api.request_id), Err(error) => eprintln!("{error}"),}Error codes: InvalidRequest, Unauthorized, NotFound, FileNotFound,
PayloadTooLarge, LimitsExceeded, ProfileNotAllowed,
CapacityExhausted, RateLimited, ShuttingDown, Cancelled,
DeadlineExceeded, SandboxSetupFailed, CacheDisabled, InternalError,
and Other(String) for codes this version does not know (treat them like
InternalError).
Examples
Section titled “Examples”The examples live in
sdks/rust/examples
and read FABER_URL and FABER_API_KEY:
export FABER_URL=http://localhost:3000 FABER_API_KEY=secretcargo run --example hello_world # health check and one commandcargo run --example compile_c # compile C, run it with stdincargo run --example cached_test_cases # cached compile step, parallel test casescargo run --example upload_file # upload, use as a task input, download, deletecargo run --example errors_and_limits # timeout, OOM and policy violation outcomes, API errorsTesting
Section titled “Testing”cargo test # unit, contract-fixture and mocked-HTTP testsFABER_URL=http://localhost:3000 FABER_API_KEY=secret cargo test --test integrationThe live integration tests are skipped unless FABER_URL is set.