Qubittron Bastion
TypeScript SDKGuides

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:

  1. Compose headers from Accept, User-Agent, defaultHeaders, per-call headers (in that order).
  2. Set Content-Type to application/json for JSON bodies; strip it for multipart so the runtime can set its own boundary.
  3. Stringify the body (or pass through FormData for multipart).
  4. Apply Authorization: Bearer <apiKey> last — the SDK overwrites any defaultHeaders.Authorization. (The header is visible to your fetch wrapper at this point; do not log it.)
  5. Call your fetch.
  6. On a network error, throw APIConnectionError. On a non-2xx, parse the body and throw the matching BastionError subclass.

That predictable shape makes a custom fetch a safe place for cross-cutting concerns.

On this page