Skip to content

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.Context on 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.
Terminal window
go get github.com/Fractal-Tess/faber/sdks/go

The package name is faber:

import faber "github.com/Fractal-Tess/faber/sdks/go"
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)
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}),
)
OptionDefaultMeaning
New(baseURL, ...)Service root or API root; /api/v1 is added when missing. A trailing slash is fine.
WithAPIKey(key)noneSent as Authorization: Bearer <key>. Every route except /health needs it.
WithTimeout(d)60 sPer attempt, until the response body is read; 0 disables it.
WithRetries(n)2Retries after a 503, a 429 or a connection failure; 0 disables them.
WithBackoff(initial, max)250 ms, 10 sFirst 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.

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.

MethodRouteReturns
Execute(ctx, ExecuteRequest)POST /execute*ExecuteResponse
Run(ctx, TaskSpec)POST /executethe *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 /metricsPrometheus text (string)
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, 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.

cleared, err := client.ClearCache(ctx) // CodeCacheDisabled if the server has no cache
text, err := client.Metrics(ctx)

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()
*RequestBuilderEffect
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.
*TaskBuilderEffect
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.

resp.AllSucceeded() // every task ran and exited 0
resp.FirstFailure() // *TaskResult or nil
resp.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 0
result.Completed() // ran to an exit status; when false, result.Error says why not
result.Stdout, result.Stderr, result.ExitCode
result.Stats.Outcome // OutcomeExited, OutcomeTimedOut, OutcomeOutOfMemory, OutcomePolicyViolation, ...
result.Stats.TerminationSignal, result.Stats.MemoryPeakBytes, result.Stats.ExecutionTimeMs
result.Cached // replayed from the step cache

TaskResult merges the contract’s CompletedTaskResult and FailedTaskResult: a failed result has a non-empty Error and no output or exit code.

Error responses are returned as *faber.Error:

FieldMeaning
StatusCodeHTTP status.
CodeErrorCode; empty when the body was not a Faber error body (e.g. from a proxy).
MessageHuman-readable message.
RequestIDx-request-id response header.
RetryAfterRetry-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 retry
case 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.

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

Terminal window
export FABER_URL=http://localhost:3000 FABER_API_KEY=secret
go run ./examples/hello-world # health check and one command
go run ./examples/compile-c # compile C, run it with stdin
go run ./examples/cached-test-cases # cached compile step, parallel test cases
go run ./examples/upload-file # upload, use as a task input, download, delete
go run ./examples/errors-and-limits # timeout, OOM and policy violation outcomes, API errors
Terminal window
go test ./... # unit, contract-fixture and httptest tests
FABER_URL=http://localhost:3000 FABER_API_KEY=secret go test -run Live ./...

The live tests are skipped unless FABER_URL is set.