Go SDK
The Go SDK is a client for the Faber API built on net/http and
encoding/json only. It covers every operation, has typed models for the
whole contract, retries transient failures, and ships five runnable
examples.
context.Contexton every call; functional options for configuration.- String enums (
TaskOutcome,ErrorCode, …) accept values this version does not know, and unknown JSON fields are ignored. - Go 1.22 or newer.
Install
Section titled “Install”go get github.com/Fractal-Tess/faber/sdks/goThe package name is faber:
import faber "github.com/Fractal-Tess/faber/sdks/go"Quick start
Section titled “Quick start”client, err := faber.New("http://localhost:3000", faber.WithAPIKey("my-api-key"))if err != nil { log.Fatal(err)}
req := faber.NewRequest(). StopOnFailure(true). Task(faber.NewTask("gcc", "-O2", "-o", "main", "main.c"). TextFile("main.c", "#include <stdio.h>\nint main(void){puts(\"hi\");}\n"). Cache(true)). Task(faber.NewTask("./main").Profile(faber.ProfileNativeV2)). Build()
resp, err := client.Execute(ctx, req)if err != nil { log.Fatal(err) // the request failed; task failures are in resp}if failure := resp.FirstFailure(); failure != nil { log.Fatalf("failed: %s %s", failure.Stats.Outcome, failure.Stderr)}fmt.Print(resp.Steps[1].Tasks[0].Stdout)Configure
Section titled “Configure”client, err := faber.New("https://faber.example.com", faber.WithAPIKey("my-api-key"), faber.WithTimeout(30*time.Second), faber.WithRetries(4), faber.WithBackoff(500*time.Millisecond, 30*time.Second), faber.WithHeader("X-Tenant", "acme"), faber.WithHTTPClient(&http.Client{Transport: myTransport}),)| Option | Default | Meaning |
|---|---|---|
New(baseURL, ...) | Service root or API root; /api/v1 is added when missing. A trailing slash is fine. | |
WithAPIKey(key) | none | Sent as Authorization: Bearer <key>. Every route except /health needs it. |
WithTimeout(d) | 60 s | Per attempt, until the response body is read; 0 disables it. |
WithRetries(n) | 2 | Retries after a 503, a 429 or a connection failure; 0 disables them. |
WithBackoff(initial, max) | 250 ms, 10 s | First wait (doubled per retry, with jitter) and longest wait, which also caps Retry-After. |
WithHeader(key, value) | Extra header on every request. Invalid names or values make New fail. | |
WithHTTPClient(c) | &http.Client{} | Your own client (shared transport, proxy, TLS, tracing). |
faber.NewFromEnv(opts...) reads FABER_URL and, if set, FABER_API_KEY.
A *Client is safe for concurrent use.
The context you pass bounds the whole call, retries and backoff included;
WithTimeout bounds each attempt.
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 the backoff; both are capped
at the longest wait. Other 5xx responses, every other 4xx response and
timeouts are never retried. An upload from a plain
io.Reader is sent once; from an io.ReadSeeker, a path or a byte slice it
is retried like any other request.
Operations
Section titled “Operations”| Method | Route | Returns |
|---|---|---|
Execute(ctx, ExecuteRequest) | POST /execute | *ExecuteResponse |
Run(ctx, TaskSpec) | POST /execute | the *TaskResult of a one-task request |
Health(ctx) | GET /health | *HealthResponse |
Info(ctx) | GET /info | *ServiceInfo |
UploadBytes(ctx, name, data, opts...) | POST /file | *UploadResponse |
UploadPath(ctx, path, opts...) | POST /file | *UploadResponse |
UploadFile(ctx, name, reader, opts...) | POST /file | *UploadResponse |
ListFiles(ctx) | GET /file | []FileInfo |
DownloadFile(ctx, id) | GET /file/{id} | []byte |
DownloadFileTo(ctx, id, w) | GET /file/{id} | bytes written (int64) |
DeleteFile(ctx, id) | DELETE /file/{id} | error |
ClearCache(ctx) | DELETE /cache | *CacheClearResponse |
Metrics(ctx) | GET /metrics | Prometheus text (string) |
Execute
Section titled “Execute”resp, err := client.Execute(ctx, req)fmt.Println(resp.Cache.Enabled, resp.Cache.Hits, resp.Cache.Stored)
// One task, one result:result, err := client.Run(ctx, faber.NewTask("echo", "hello"))fmt.Print(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”health, err := client.Health(ctx)if err == nil && !health.OK() { // 503 while draining is returned here, not as an error, and not retried.}
info, err := client.Info(ctx)fmt.Println(info.Version, info.Arch, info.SandboxProfiles.Default)fmt.Println(info.Limits.MemoryMaxBytes, info.Limits.MaxParallelTasks)for _, toolchain := range info.Toolchains { if toolchain.Version != nil { // nil when the image does not record a version fmt.Println(toolchain.ID, *toolchain.Version) }}uploaded, err := client.UploadBytes(ctx, "input.txt", []byte("1 2\n"), faber.WithContentType("text/plain"))_, err = client.UploadPath(ctx, "testdata/big-input.txt") // streamed, stored as "big-input.txt"_, err = client.UploadFile(ctx, "stdin.txt", os.Stdin) // any io.Reader
files, err := client.ListFiles(ctx) // ID, Filename, ContentType, Size, CreatedAt (time.Time)data, err := client.DownloadFile(ctx, uploaded.FileID)_, err = client.DownloadFileTo(ctx, uploaded.FileID, outFile)err = client.DeleteFile(ctx, uploaded.FileID)Uploading the same bytes again returns the same FileID with
AlreadyExists set. File ids that are not alphanumeric are rejected with
faber.ErrInvalidFileID before anything is sent.
Cache and metrics
Section titled “Cache and metrics”cleared, err := client.ClearCache(ctx) // CodeCacheDisabled if the server has no cachetext, err := client.Metrics(ctx)Building requests
Section titled “Building requests”NewRequest and NewTask return fluent builders. Task, Step,
ExecuteRequest and FileInput are plain structs, so struct literals work
too; a Task value and a *TaskBuilder are both a TaskSpec.
req := faber.NewRequest(). Cache(false). StopOnFailure(true). Task(faber.NewTask("gcc", "-O2", "-o", "main", "main.c"). TextFile("main.c", source). // UTF-8 text BinaryFile("data/blob.bin", []byte{0, 1, 2}). // base64 on the wire File("run.sh", faber.Text(script).AsExecutable()). // mode 0755 UploadedFile("input.txt", uploaded.FileID). // uploaded earlier Cache(true)). Parallel( faber.NewTask("./main").Stdin("1 2\n").Profile(faber.ProfileNativeV2), faber.NewTask("./main").Stdin("40 2\n").Env("LANG", "C"), ). Task(faber.NewTask("sh", "-c", "ls").WorkingDir("/faber/data")). Build()*RequestBuilder | Effect |
|---|---|
Task(TaskSpec) | Appends a single-task step. |
Parallel(TaskSpec...) | Appends a step whose tasks run concurrently. |
Step(Step) | Appends a step built with SingleStep or ParallelStep. |
Cache(bool) | Default for tasks that do not set their own flag. |
StopOnFailure(bool) | After a failing task, later steps are reported as not_started. |
Build() | Returns the ExecuteRequest. |
*TaskBuilder | Effect |
|---|---|
NewTask(cmd, args...), Args(...) | Program and arguments. |
Env(key, value) | Environment variable. |
Stdin(text) | Standard input. |
TextFile, BinaryFile, UploadedFile, File(path, FileInput) | Files created before the task starts. |
WorkingDir(dir) | Start directory (default /faber). |
Profile(p) | ProfileCompileV2, ProfileNativeV2, ProfileCompileV1, ProfileNativeV1. |
Cache(bool) | Marks the task deterministic for the step cache. |
FileInput values come from faber.Text, faber.Binary and
faber.Uploaded; .AsExecutable() sets mode 0755. Optional flags on the
structs are *bool; faber.Bool(true) makes one.
Results
Section titled “Results”resp.AllSucceeded() // every task ran and exited 0resp.FirstFailure() // *TaskResult or nilresp.Tasks() // every task, parallel steps flattened
step := resp.Steps[1] // StepResult{Tasks []TaskResult, Parallel bool}step.Succeeded()result := step.Tasks[0]
result.Succeeded() // ran and exited 0result.Completed() // ran to an exit status; when false, result.Error says why notresult.Stdout, result.Stderr, result.ExitCoderesult.Stats.Outcome // OutcomeExited, OutcomeTimedOut, OutcomeOutOfMemory, OutcomePolicyViolation, ...result.Stats.TerminationSignal, result.Stats.MemoryPeakBytes, result.Stats.ExecutionTimeMsresult.Cached // replayed from the step cacheTaskResult merges the contract’s CompletedTaskResult and
FailedTaskResult: a failed result has a non-empty Error and no output or
exit code.
Errors
Section titled “Errors”Error responses are returned as *faber.Error:
| Field | Meaning |
|---|---|
StatusCode | HTTP status. |
Code | ErrorCode; empty when the body was not a Faber error body (e.g. from a proxy). |
Message | Human-readable message. |
RequestID | x-request-id response header. |
RetryAfter | Retry-After response header, zero when absent. |
Transport failures (connection refused, timeouts, cancelled contexts) and
decoding failures are returned as wrapped errors, so errors.Is(err, context.DeadlineExceeded) works.
resp, err := client.Execute(ctx, req)switch {case faber.IsCode(err, faber.CodeFileNotFound): // upload the file, then retrycase err != nil: var apiErr *faber.Error if errors.As(err, &apiErr) { log.Printf("%d %s: %s [request %s]", apiErr.StatusCode, apiErr.Code, apiErr.Message, apiErr.RequestID) } return err}Error codes: CodeInvalidRequest, CodeUnauthorized, CodeNotFound,
CodeFileNotFound, CodePayloadTooLarge, CodeLimitsExceeded,
CodeProfileNotAllowed, CodeCapacityExhausted, CodeRateLimited,
CodeShuttingDown, CodeCancelled, CodeDeadlineExceeded,
CodeSandboxSetupFailed, CodeCacheDisabled, CodeInternalError. Treat
unknown codes like CodeInternalError.
Examples
Section titled “Examples”The examples live in
sdks/go/examples
and read FABER_URL and FABER_API_KEY:
export FABER_URL=http://localhost:3000 FABER_API_KEY=secretgo run ./examples/hello-world # health check and one commandgo run ./examples/compile-c # compile C, run it with stdingo run ./examples/cached-test-cases # cached compile step, parallel test casesgo run ./examples/upload-file # upload, use as a task input, download, deletego run ./examples/errors-and-limits # timeout, OOM and policy violation outcomes, API errorsTesting
Section titled “Testing”go test ./... # unit, contract-fixture and httptest testsFABER_URL=http://localhost:3000 FABER_API_KEY=secret go test -run Live ./...The live tests are skipped unless FABER_URL is set.