Custom fetch
Use the fetch hook to add timeouts, proxies, tracing, and request mocking.
new Bastion({ fetch }) accepts any function with the standard fetch signature. The SDK uses it for every HTTP call (regular and streaming), so anything you bolt onto it applies uniformly.
new Bastion({
apiKey: process.env.BASTION_API_KEY,
fetch: (url, init) => globalThis.fetch(url, init), // identity — the default
});Per-request timeout
fetch itself has no timeout. Wrap it with an AbortController:
const withTimeout = (ms: number): typeof globalThis.fetch =>
(url, init) => {
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), ms);
return globalThis.fetch(url, { ...init, signal: ac.signal }).finally(() =>
clearTimeout(timer),
);
};
const client = new Bastion({
apiKey: process.env.BASTION_API_KEY,
fetch: withTimeout(30_000),
});For streaming calls, the timeout fires from the moment the request starts — including the time spent iterating the stream. If you want a "time-to-first-byte" timeout, gate the signal on a flag you flip when iteration begins.
Proxy
In Node, use undici's ProxyAgent:
import { fetch, ProxyAgent, type RequestInit } from "undici";
const dispatcher = new ProxyAgent("http://proxy.internal:8080");
const client = new Bastion({
apiKey: process.env.BASTION_API_KEY,
fetch: ((url, init) =>
fetch(url, { ...(init as RequestInit), dispatcher })) as typeof globalThis.fetch,
});undici's fetch is a near-superset of the spec — the dispatcher field is the proxy hook.
Tracing / observability
Wrap fetch to attach request ids and time each call:
const traced: typeof globalThis.fetch = async (url, init) => {
const requestId = crypto.randomUUID();
const started = performance.now();
const headers = new Headers(init?.headers);
headers.set("x-trace-id", requestId);
try {
const res = await globalThis.fetch(url, { ...init, headers });
metrics.observe("bastion_call_ms", performance.now() - started, {
status: String(res.status),
});
return res;
} catch (err) {
metrics.increment("bastion_call_error", { requestId });
throw err;
}
};
const client = new Bastion({ apiKey: process.env.BASTION_API_KEY, fetch: traced });The SDK applies Authorization: Bearer <apiKey> last, just before invoking your custom fetch. By then the header is in init.headers — your wrapper can read it (don't log it) but defaultHeaders.Authorization cannot leak a wrong key onto a request, because the SDK overwrites it.
Mocking in tests
The cleanest way to test code that uses the SDK: inject a fake fetch.
import { Bastion } from "@qubittron/bastion-sdk";
import { expect, test } from "vitest";
test("happy path", async () => {
const fakeFetch: typeof globalThis.fetch = async () =>
new Response(
JSON.stringify({
id: "cmpl_x",
object: "chat.completion",
created: 0,
model: "gpt-oss-120b",
choices: [
{
index: 0,
message: { role: "assistant", content: "ok" },
finish_reason: "stop",
},
],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
const client = new Bastion({ apiKey: "test", fetch: fakeFetch });
const res = await client.chat.completions.create({
model: "gpt-oss-120b",
messages: [{ role: "user", content: "hi" }],
});
expect(res.choices[0]?.message.content).toBe("ok");
});For streaming, return a Response whose body is a ReadableStream<Uint8Array> emitting SSE-formatted bytes.
Order of operations inside the SDK
For each request:
- Compose
headersfromAccept,User-Agent,defaultHeaders, per-call headers (in that order). - Set
Content-Typetoapplication/jsonfor JSON bodies; strip it for multipart so the runtime can set its own boundary. - Stringify the body (or pass through
FormDatafor multipart). - Apply
Authorization: Bearer <apiKey>last — the SDK overwrites anydefaultHeaders.Authorization. (The header is visible to your fetch wrapper at this point; do not log it.) - Call your
fetch. - On a network error, throw
APIConnectionError. On a non-2xx, parse the body and throw the matchingBastionErrorsubclass.
That predictable shape makes a custom fetch a safe place for cross-cutting concerns.