Skip to content
Fuse
Esc
navigateopen⌘Jpreview
On this page

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.

Was this page helpful?