csar-ts
v1.1.1
Published
Smart Resilience SDK — lightweight interceptor for Axios/Fetch with transparent backpressure handling from the CSAR API gateway
Downloads
37
Maintainers
Readme
csar-ts
Smart resilience SDK for the CSAR API Router — transparent backpressure handling, circuit breaking, and request deduplication for Axios and Fetch.
What is CSAR?
CSAR is a high-performance API gateway and router that implements cooperative rate limiting (backpressure). Instead of silently dropping requests, CSAR responds with 503 and tells the client exactly how long to wait, so traffic can be absorbed gracefully rather than lost.
This SDK wires up that protocol transparently so your application code never has to think about it.
Features
- Transparent backpressure — automatically retries
503responses with the wait time the router specifies (X-CSAR-Wait-MS/Retry-After) - Server circuit breaker detection — reads
X-CSAR-Status: circuit_openand throws immediately instead of retrying - Client-side circuit breaker — tracks consecutive 5xx errors per origin and short-circuits locally before hitting the network
- Request deduplication — collapses identical in-flight
GETrequests into a single network call X-CSAR-Client-Limitinjection — advertises your client's RPS capacity to the router for proactive shaping- OpenTelemetry-compatible tracing — generates
X-Request-Idand W3Ctraceparentheaders per request - Built-in STS authentication — loads service keys, signs JWT assertions, exchanges them for access tokens, and retries once on
401 - High-level CSAR client —
createCsarClient()gives you a base-URL-aware client with resilience and auth built in - Composable middleware pipeline — clean, testable architecture for the Fetch adapter; same logic reused in Axios interceptors
- Async auth-ready adapters —
withCsarFetchAsync()andapplyCsarAxiosAsync()enable auth-aware setup - Dual ESM / CJS build — works in Node.js, Bun, browsers, and edge runtimes
- Minimal runtime dependencies — Axios stays an optional peer dep;
josepowers JWT signing for STS auth
Installation
# npm
npm install csar-ts
# bun
bun install csar-tsIf you use the Axios adapter, install Axios alongside it:
bun install axios csar-tsQuick Start
Fetch
import { withCsarFetch } from "csar-ts";
const fetch = withCsarFetch(globalThis.fetch, {
maxWaitMs: 5000, // give up if router asks to wait more than 5s
maxRetries: 3, // retry up to 3 times on 503
clientLimitRps: 50, // tell the router our max throughput
debug: true,
});
// Use exactly like the native fetch API — backpressure is handled automatically
const res = await fetch("https://api.example.com/data");Fetch + STS Auth
import { withCsarFetchAsync } from "csar-ts";
const fetch = await withCsarFetchAsync(globalThis.fetch, {
maxWaitMs: 5000,
maxRetries: 3,
auth: {
stsEndpoint: "https://auth.example.com/sts/token",
keyFile: "./authorized_key.json",
accessTokenAudience: "orders-service",
},
});
const res = await fetch("https://api.example.com/orders");Axios
import axios from "axios";
import { applyCsarAxios } from "csar-ts";
const instance = axios.create({ baseURL: "https://api.example.com" });
applyCsarAxios(instance, {
maxWaitMs: 5000,
maxRetries: 3,
clientLimitRps: 50,
debug: true,
});
// Use instance normally — interceptors handle everything
const { data } = await instance.get("/data");Axios + STS Auth
import axios from "axios";
import { applyCsarAxiosAsync } from "csar-ts";
const instance = axios.create({ baseURL: "https://api.example.com" });
await applyCsarAxiosAsync(instance, {
maxWaitMs: 5000,
maxRetries: 3,
auth: {
stsEndpoint: "https://auth.example.com/sts/token",
keyData: serviceKey,
},
});
const { data } = await instance.get("/orders");High-Level Client
import { createCsarClient } from "csar-ts";
const client = await createCsarClient({
baseUrl: "https://api.example.com",
maxWaitMs: 5000,
maxRetries: 3,
auth: {
stsEndpoint: "https://auth.example.com/sts/token",
keyFile: "./authorized_key.json",
},
});
const res = await client.get("/v1/data");Configuration
interface CsarConfig {
/** Maximum ms the client is willing to wait before throwing. Required. */
maxWaitMs: number;
/** Number of retry attempts on 503. Required. */
maxRetries: number;
/**
* Client RPS hint sent as `X-CSAR-Client-Limit` on every request.
* Allows the router to shape traffic proactively.
*/
clientLimitRps?: number;
/** Enable debug logging to console. */
debug?: boolean;
/** Client-side circuit breaker settings. */
circuitBreaker?: {
/** Consecutive 5xx failures before the local circuit opens. */
threshold: number;
/** Ms the circuit stays open before allowing a probe request. */
resetTimeoutMs: number;
};
/**
* Collapse identical in-flight GET requests into one network call.
* All callers receive the same response object. Default: false.
*/
dedup?: boolean;
/**
* Generate and inject `X-Request-Id` + W3C `traceparent` on every request.
* Enables end-to-end tracing from client to upstream service.
*/
generateTraceId?: boolean;
/**
* Optional STS authentication.
* When provided, async adapters and `createCsarClient()` automatically
* obtain and inject Bearer tokens.
*/
auth?: {
/** URL of the Security Token Service endpoint. */
stsEndpoint: string;
/** Path to `authorized_key.json` (Node.js only). */
keyFile?: string;
/** In-memory key object for edge/browser/server runtimes. */
keyData?: CsarServiceKey;
/** Audience for the JWT assertion. Defaults to `stsEndpoint`. */
stsAudience?: string;
/** Audience for the requested access token (sent as form field to STS). */
accessTokenAudience?: string;
/** @deprecated Use `stsAudience` instead. */
audience?: string;
/** Retry count for failed token exchanges. Default: 2. */
maxStsRetries?: number;
};
/** Called just before each retry sleep. */
onRetry?: (delayMs: number, attempt: number, error: unknown) => void;
}auth.keyFile is Node.js-only. In browsers, edge runtimes, or any environment where reading from disk is unavailable, pass auth.keyData instead.
CSAR Protocol Headers
| Header | Direction | Description |
|---|---|---|
| X-CSAR-Wait-MS | Response | Milliseconds the request spent queued in the throttle |
| X-CSAR-Status | Response | Machine-readable state: throttled, circuit_open, circuit_half_open |
| X-CSAR-Client-Limit | Request | Client's self-reported max RPS capacity |
| Retry-After | Response | Standard RFC 7231 fallback (seconds) if X-CSAR-Wait-MS is absent |
A 503 response means backpressure. The SDK classifies it by priority:
X-CSAR-Status: circuit_open/circuit_half_open→ throwCsarCircuitBrokenErrorimmediatelyX-CSAR-Status: throttled→ waitX-CSAR-Wait-MSms, then retry- No
X-CSAR-Statusheader → checkX-CSAR-Wait-MS, fall back toRetry-After, fall back to 1 s - Response body contains
"circuit breaker open"→ throwCsarCircuitBrokenError(legacy fallback)
Error Handling
import {
CsarBackpressureError,
CsarCircuitBrokenError,
CsarAuthError,
} from "csar-ts";
try {
const res = await fetch("https://api.example.com/data");
} catch (err) {
if (err instanceof CsarCircuitBrokenError) {
// Source tells you who tripped the circuit
console.error(`Circuit broken (${err.source}):`, err.message);
// err.source === "server" — X-CSAR-Status said circuit_open
// err.source === "client" — local CB tripped after N consecutive 5xx
}
if (err instanceof CsarBackpressureError) {
// maxWaitMs exceeded or maxRetries exhausted
console.error(
`Backpressure unabsorbed after ${err.attempt} attempts`,
`(router requested ${err.requestedWaitMs}ms)`,
);
}
if (err instanceof CsarAuthError) {
console.error(`Auth failed [${err.code}]`, err.message);
}
}Authentication Flow
When auth is configured, the SDK:
- Loads and validates the service key from
keyFileorkeyData - Signs a short-lived JWT assertion using
EdDSAorRS256 - Exchanges that assertion with the CSAR STS for an access token
- Caches the token in memory and refreshes it before expiry
- Retries once on
401after forcing a token refresh
Advanced: Full Config Example
import {
withCsarFetchAsync,
CsarBackpressureError,
CsarCircuitBrokenError,
} from "csar-ts";
const fetch = await withCsarFetchAsync(globalThis.fetch, {
maxWaitMs: 8000,
maxRetries: 5,
clientLimitRps: 100,
debug: process.env.NODE_ENV !== "production",
// Client-side circuit breaker — trips after 3 consecutive 5xx per origin
circuitBreaker: {
threshold: 3,
resetTimeoutMs: 30_000, // try again after 30s
},
// Collapse duplicate in-flight GETs (great for dashboard polling)
dedup: true,
// Inject X-Request-Id + traceparent for distributed tracing
generateTraceId: true,
// Obtain Bearer tokens from the CSAR STS
auth: {
stsEndpoint: "https://auth.example.com/sts/token",
keyFile: "./authorized_key.json",
accessTokenAudience: "payments-service",
maxStsRetries: 2,
},
// Log every retry to your monitoring pipeline
onRetry: (delayMs, attempt, error) => {
console.warn(`[csar] retry #${attempt} in ${delayMs}ms`, error);
},
});Advanced: Custom Middleware Pipeline
The Fetch adapter is built on a composable middleware system you can extend:
import {
composeFetchPipeline,
createHeadersMiddleware,
createCircuitBreakerMiddleware,
createDedupMiddleware,
createRetryMiddleware,
createLogger,
type FetchMiddleware,
type CsarConfig,
} from "csar-ts";
const config: CsarConfig = { maxWaitMs: 5000, maxRetries: 3 };
const log = createLogger(true);
// Add your own middleware
const authMiddleware: FetchMiddleware = (input, init, next) => {
const headers = new Headers(init?.headers);
headers.set("Authorization", `Bearer ${getToken()}`);
return next(input, { ...init, headers });
};
const fetch = composeFetchPipeline(
[
createHeadersMiddleware(config),
authMiddleware, // ← your custom step
createDedupMiddleware(config, log),
createCircuitBreakerMiddleware(config, log),
createRetryMiddleware(config, log),
],
globalThis.fetch,
);How It Fits Together
Your App
│
▼
withCsarFetch / applyCsarAxios
│
├── Headers middleware (X-CSAR-Client-Limit, X-Request-Id, traceparent)
├── Dedup middleware (collapse identical in-flight GETs)
├── Client CB middleware (pre-flight check, post-response tracking)
└── Retry middleware (503 → classify → wait → retry)
│
▼
CSAR API Gateway (github.com/ledatu/csar)
│
▼
Your upstreamBuilding & Testing
# Build ESM + CJS + .d.ts
npm run build
# Run tests
npm test
# Type-check only
npm run lintRelated
- ledatu/csar — router source code
License
MIT
