Advanced usage
Custom fetch, retries, request correlation, and testing
Custom fetch
fetch replaces the implementation used for every request, including the event
stream. Use it to add logging, route through a proxy, or supply a polyfill on a
runtime without a global fetch:
const client = new FuseClient({
baseUrl: "https://orchestrator.example.com",
token,
fetch: (input, init) => {
console.log("fetching", input);
return fetch(input, init);
},
});
If no global fetch exists and no fetch option is supplied, the client throws
a FuseError at construction time rather than failing later on the first
request.
Retries
The SDK does not retry requests itself, a failed call rejects immediately, either
with a FuseError (transport/configuration) or a FuseApiError (a non-2xx
response). Retry policy is callsite-specific, so wrap calls yourself:
import { isUnavailable, FuseApiError } from "@folsom/fuse";
async function createWithRetry(client: FuseClient, body: CreateRequest) {
let lastErr: unknown;
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await client.environments.create(body);
} catch (err) {
lastErr = err;
if (!isUnavailable(err)) throw err; // not retryable
await new Promise((r) => setTimeout(r, (attempt + 1) * 1000));
}
}
throw 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
requestId is a function 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 FuseApiError.requestId. The server echoes it back
only if it is 1 to 128 characters of [A-Za-z0-9_-], anything else is silently
replaced with a server-generated req_<32hex>, so a trace ID containing .,
:, or / is dropped rather than propagated. randomUUID() below is safe.
Use it to thread a trace ID from your own service into the orchestrator’s logs
and audit events:
import { randomUUID } from "node:crypto";
const client = new FuseClient({
baseUrl,
token,
requestId: () => randomUUID(),
});
An empty string from the function omits the header entirely rather than sending a blank one.
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 FuseClient at http://localhost:8080 with no token (the orchestrator
also runs without auth when ORCH_AUTH_TOKEN is unset), and every SDK call
behaves exactly as it would against a real deployment, states transition, events
stream, snapshots and forks work, just nothing is actually virtualized
underneath. The one exception is client.apiKeys.*, which throws 404 /
route_not_found here, key management needs a Postgres-backed store and its
routes are not mounted without DATABASE_URL.
For unit-testing your own code that calls the SDK, spin up a real local server
with Node’s http module (the same approach the SDK’s own test suite uses) and
point a FuseClient at it:
import { createServer } from "node:http";
import { FuseClient } from "@folsom/fuse";
const server = createServer((req, res) => {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ id: "vm-1", state: "running", task_id: "task-1", url: "u" }));
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address() as { port: number };
const client = new FuseClient({ baseUrl: `http://127.0.0.1:${port}`, token: "tok" });
const env = await client.environments.create({ task_id: "task-1" });
// assert on env, then server.close()
Any HTTP mocking library that intercepts the global fetch (msw, nock’s
fetch interceptor, and so on) works too, the SDK has no dependency on a specific
one.
Choosing a timeout
timeoutMs at the client level is a sensible default 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
max_runtime_seconds 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. timeoutMs is never applied to events(),
that stream has no built-in timeout by design, use { signal } to bound it
instead.