Advanced usage
Custom transports, retries, request correlation, and testing
Custom HTTP client
WithHTTPClient replaces the client used for every normal (non-streaming)
request. Use it to set a shorter timeout, add a custom http.RoundTripper for
logging or metrics, or route through a proxy:
client, err := fuse.New(baseURL, token,
fuse.WithHTTPClient(&http.Client{
Timeout: 10 * time.Second,
Transport: myLoggingTransport{http.DefaultTransport},
}),
)
This does not affect the event stream. Events always uses a separate,
internally managed client with no timeout, since a request-level deadline would
kill a long-lived SSE connection. Cancel the context.Context you pass to
Events to stop it instead.
Retries
The SDK does not retry requests itself, a failed request returns immediately as
either a network error or an *APIError. This is deliberate: retry policy
(which codes are safe to retry, backoff shape, idempotency) is callsite-specific,
and baking one policy into the client would be wrong for some callers. Wrap calls
yourself:
func createWithRetry(ctx context.Context, c *fuse.Client, req fuse.CreateRequest) (*fuse.EnvironmentInfo, error) {
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
env, err := c.Environments.Create(ctx, req)
if err == nil {
return env, nil
}
lastErr = err
if !fuse.IsUnavailable(err) {
return nil, err // not retryable
}
time.Sleep(time.Duration(attempt+1) * time.Second)
}
return nil, lastErr
}
IsUnavailable and IsConflict are usually the two codes worth retrying (the
orchestrator rejecting work while shutting down, or a transient scheduling
conflict); IsInvalidArgument and IsNotFound are not, retrying won’t change the
outcome.
Request correlation
WithRequestID sets a generator called once per request. Its return value is
sent as X-Request-ID and echoed back on every response, including error
responses, where it also appears on APIError.RequestID. Use it to thread a
trace ID from your own service into the orchestrator’s logs and audit events:
client, _ := fuse.New(baseURL, token,
fuse.WithRequestID(func() string {
return uuid.NewString()
}),
)
An empty return value from the generator omits the header entirely rather than sending a blank one.
The orchestrator echoes the supplied ID only if it is 1 to 128 characters of
[A-Za-z0-9_-], anything else is silently replaced with a server-generated
req_<hex> ID rather than rejected. Trace formats containing ., :, or /
are therefore dropped without any error, normalize them before returning them
from the generator.
Testing against the stub
The orchestrator itself, not the SDK, has a built-in in-memory stub provider: run
it with no FIRECRACKER_BASE_URL set and it simulates VM lifecycle behavior
without booting real microVMs. This is the fastest way to test SDK-driven code
end to end without a real Firecracker host:
go build -o bin/orchestrator ./orchestrator
./bin/orchestrator # FIRECRACKER_BASE_URL unset -> stub mode, listens on :8080
Point a fuse.New client at http://localhost:8080 with no token (the
orchestrator also runs without auth when ORCH_AUTH_TOKEN is unset), and
Environments and Snapshots behave exactly as they would against a real
deployment, states transition, events stream, snapshots and forks work
(Firecracker-backed stub VMs support both), just nothing is actually virtualized
underneath.
Two services do not. client.APIKeys.* returns a 404 route_not_found
unless the orchestrator was started with DATABASE_URL, since the
/v1/api-keys routes are registered only with a Postgres-backed key store. And
Hosts.Register never uses the stub: it always builds a real provider against
the url you pass, so with no host agent actually listening there the capacity
probe fails and registration returns a 502 if you left
cpus/ram_mb/storage_gb at 0. Declare all three explicitly to register a
host in stub mode.
For unit-testing your own code that calls the SDK, httptest.Server is the
pattern the SDK’s own test suite uses, point fuse.New at the test server’s URL
and assert on what it received:
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"id":"vm-1","state":"running","task_id":"task-1","url":"https://x"}`)
}))
defer srv.Close()
client, _ := fuse.New(srv.URL, "test-token")
env, err := client.Environments.Create(context.Background(), fuse.CreateRequest{TaskID: "task-1"})
Choosing a timeout
The default 60-second timeout on the normal-request client is generous enough
for every non-streaming call, including Create, which blocks server-side until
the environment reaches running or fails. If you’re provisioning environments
with a large MaxRuntimeSeconds or a slow custom image pull, you generally don’t
need a longer client timeout for that, Create itself doesn’t wait for the whole
runtime, only for the initial boot. Where you do want a shorter timeout is
read-only calls like List/Get in a hot path, set one via WithHTTPClient
rather than wrapping every call in your own context.WithTimeout.