Skip to content

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 reqwest with rustls (no OpenSSL) and tokio.
  • 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.
[dependencies]
faber-sdk = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

The library is imported as faber_sdk.

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(())
}
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 methodDefaultMeaning
Client::builder(url)Service root or API root; /api/v1 is added when missing. A trailing slash is fine.
.api_key(key)noneSent as Authorization: Bearer <key>. Every route except /health needs it.
.timeout(duration) / .no_timeout()60 sPer attempt, from connecting until the response body is read.
.retry(RetryPolicy)2 retries, 250 ms, 10 sSee Retries.
.header(name, value)Extra header on every request. Invalid names or values fail build().
.http_client(reqwest::Client)rustls clientYour 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.

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();
MethodRouteReturns
execute(&ExecuteRequest)POST /executeExecuteResponse
run(Task)POST /executethe TaskResult of a one-task request
health()GET /healthHealthResponse
info()GET /infoServiceInfo
upload_file(FileUpload)POST /fileUploadResponse
list_files()GET /fileVec<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 /cacheCacheClearResponse
metrics()GET /metricsPrometheus text (String)
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.

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 disk
client.upload_file(FileUpload::reader(tokio::io::stdin())).await?; // any AsyncRead
client.upload_file(FileUpload::stream(response.bytes_stream())).await?; // any TryStream of bytes
let files = client.list_files().await?; // id, filename, content_type, size, created_at
let 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.

let cleared = client.clear_cache().await?; // Err with code cache_disabled if the server has no cache
let text = client.metrics().await?;

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"));
ExecuteRequestEffect
.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.
TaskEffect
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.

response.all_succeeded(); // every task ran and exited 0
response.first_failure(); // Option<&TaskResult>
response.tasks(); // iterator over every task, parallel steps flattened
let step = &response.steps[1]; // StepResult::Task or StepResult::Parallel
step.is_parallel();
step.succeeded();
let result = &step.tasks()[0]; // tasks() is a slice for both shapes
result.succeeded(); // ran and exited 0
result.exit_code(); // Option<i32>; None when the task never ran
result.stdout(); // "" for a task that never ran
result.stderr();
result.error(); // Option<&str>: why the task did not run
result.outcome(); // &TaskOutcome
result.stats(); // memory, CPU, pids, time, truncation, signal
result.is_cached(); // replayed from the step cache

TaskResult 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).

Every operation returns faber_sdk::Result<T> with one Error enum:

VariantWhen
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).

The examples live in sdks/rust/examples and read FABER_URL and FABER_API_KEY:

Terminal window
export FABER_URL=http://localhost:3000 FABER_API_KEY=secret
cargo run --example hello_world # health check and one command
cargo run --example compile_c # compile C, run it with stdin
cargo run --example cached_test_cases # cached compile step, parallel test cases
cargo run --example upload_file # upload, use as a task input, download, delete
cargo run --example errors_and_limits # timeout, OOM and policy violation outcomes, API errors
Terminal window
cargo test # unit, contract-fixture and mocked-HTTP tests
FABER_URL=http://localhost:3000 FABER_API_KEY=secret cargo test --test integration

The live integration tests are skipped unless FABER_URL is set.