Skip to content

Commit 8db154f

Browse files
mcollinaclaude
andcommitted
docs: add circuit breaker interceptor documentation
Document the circuit breaker interceptor in the Dispatcher API docs with configuration options, usage examples, and error handling guidance. Add CircuitBreakerError to the Errors documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 4030a2e commit 8db154f

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

docs/docs/api/Dispatcher.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,119 @@ const client = new Client("http://example.com").compose(
12021202
- Handles case-insensitive encoding names
12031203
- Supports streaming decompression without buffering
12041204

1205+
##### `circuitBreaker`
1206+
1207+
The `circuitBreaker` interceptor implements the [circuit breaker pattern](https://martinfowler.com/bliki/CircuitBreaker.html) to prevent cascading failures when upstream services are unavailable or responding with errors.
1208+
1209+
The circuit breaker has three states:
1210+
- **Closed** - Requests flow normally. Failures are counted, and when the threshold is reached, the circuit opens.
1211+
- **Open** - All requests fail immediately with `CircuitBreakerError` without contacting the upstream service.
1212+
- **Half-Open** - After the timeout period, a limited number of requests are allowed through to test if the service has recovered.
1213+
1214+
**Options**
1215+
1216+
- `threshold` - Number of consecutive failures before opening the circuit. Default: `5`.
1217+
- `timeout` - How long (in milliseconds) the circuit stays open before transitioning to half-open. Default: `30000` (30 seconds).
1218+
- `successThreshold` - Number of successful requests in half-open state needed to close the circuit. Default: `1`.
1219+
- `maxHalfOpenRequests` - Maximum number of concurrent requests allowed in half-open state. Default: `1`.
1220+
- `statusCodes` - Array or Set of HTTP status codes that count as failures. Default: `[500, 502, 503, 504]`.
1221+
- `errorCodes` - Array or Set of error codes that count as failures. Default: `['UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'UND_ERR_SOCKET', 'ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'EPIPE', 'ENOTFOUND', 'ENETUNREACH', 'EHOSTUNREACH', 'EAI_AGAIN']`.
1222+
- `getKey` - Function to extract a circuit key from request options. Default: uses origin only. Signature: `(opts: DispatchOptions) => string`.
1223+
- `storage` - Custom `CircuitBreakerStorage` instance for storing circuit states. Useful for sharing state across multiple dispatchers.
1224+
- `onStateChange` - Callback invoked when a circuit changes state. Signature: `(key: string, newState: 'open' | 'half-open' | 'closed', previousState: string) => void`.
1225+
1226+
**Example - Basic Circuit Breaker**
1227+
1228+
```js
1229+
const { Client, interceptors } = require("undici");
1230+
const { circuitBreaker } = interceptors;
1231+
1232+
const client = new Client("http://example.com").compose(
1233+
circuitBreaker({
1234+
threshold: 5,
1235+
timeout: 30000
1236+
})
1237+
);
1238+
1239+
try {
1240+
const response = await client.request({ path: "/", method: "GET" });
1241+
} catch (err) {
1242+
if (err.code === "UND_ERR_CIRCUIT_BREAKER") {
1243+
console.log("Circuit is open, service unavailable");
1244+
}
1245+
}
1246+
```
1247+
1248+
**Example - Route-Level Circuit Breakers**
1249+
1250+
Use the `getKey` option to create separate circuits for different routes:
1251+
1252+
```js
1253+
const { Agent, interceptors } = require("undici");
1254+
const { circuitBreaker } = interceptors;
1255+
1256+
const client = new Agent().compose(
1257+
circuitBreaker({
1258+
threshold: 3,
1259+
timeout: 10000,
1260+
getKey: (opts) => `${opts.origin}${opts.path}`
1261+
})
1262+
);
1263+
1264+
// /api/users and /api/products have independent circuits
1265+
await client.request({ origin: "http://example.com", path: "/api/users", method: "GET" });
1266+
await client.request({ origin: "http://example.com", path: "/api/products", method: "GET" });
1267+
```
1268+
1269+
**Example - Custom Status Codes**
1270+
1271+
Configure the circuit breaker to trip on rate limiting:
1272+
1273+
```js
1274+
const { Client, interceptors } = require("undici");
1275+
const { circuitBreaker } = interceptors;
1276+
1277+
const client = new Client("http://example.com").compose(
1278+
circuitBreaker({
1279+
threshold: 3,
1280+
statusCodes: [429, 500, 502, 503, 504]
1281+
})
1282+
);
1283+
```
1284+
1285+
**Example - State Change Monitoring**
1286+
1287+
```js
1288+
const { Client, interceptors } = require("undici");
1289+
const { circuitBreaker } = interceptors;
1290+
1291+
const client = new Client("http://example.com").compose(
1292+
circuitBreaker({
1293+
threshold: 5,
1294+
onStateChange: (key, newState, prevState) => {
1295+
console.log(`Circuit ${key}: ${prevState} -> ${newState}`);
1296+
}
1297+
})
1298+
);
1299+
```
1300+
1301+
**Error Handling**
1302+
1303+
When the circuit is open or half-open (with max requests reached), requests will fail with a `CircuitBreakerError`:
1304+
1305+
```js
1306+
const { errors } = require("undici");
1307+
1308+
try {
1309+
await client.request({ path: "/", method: "GET" });
1310+
} catch (err) {
1311+
if (err instanceof errors.CircuitBreakerError) {
1312+
console.log(`Circuit breaker triggered: ${err.state}`); // 'open' or 'half-open'
1313+
console.log(`Circuit key: ${err.key}`);
1314+
}
1315+
}
1316+
```
1317+
12051318
##### `Cache Interceptor`
12061319

12071320
The `cache` interceptor implements client-side response caching as described in

docs/docs/api/Errors.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { errors } from 'undici'
2626
| `InformationalError` | `UND_ERR_INFO` | expected error with reason |
2727
| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed |
2828
| `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed |
29+
| `CircuitBreakerError` | `UND_ERR_CIRCUIT_BREAKER` | circuit breaker is open or half-open, request rejected |
2930

3031
Be aware of the possible difference between the global dispatcher version and the actual undici version you might be using. We recommend to avoid the check `instanceof errors.UndiciError` and seek for the `error.code === '<error_code>'` instead to avoid inconsistencies.
3132
### `SocketError`
@@ -46,3 +47,24 @@ interface SocketInfo {
4647
```
4748

4849
Be aware that in some cases the `.socket` property can be `null`.
50+
51+
### `CircuitBreakerError`
52+
53+
The `CircuitBreakerError` is thrown when a request is rejected by the circuit breaker interceptor. It has the following properties:
54+
55+
- `state` - The current state of the circuit breaker when the error was thrown. Either `'open'` or `'half-open'`.
56+
- `key` - The circuit key identifying which circuit rejected the request (e.g., the origin URL).
57+
58+
```js
59+
const { errors } = require('undici')
60+
61+
try {
62+
await client.request({ path: '/', method: 'GET' })
63+
} catch (err) {
64+
if (err instanceof errors.CircuitBreakerError) {
65+
console.log(err.code) // 'UND_ERR_CIRCUIT_BREAKER'
66+
console.log(err.state) // 'open' or 'half-open'
67+
console.log(err.key) // e.g., 'http://example.com'
68+
}
69+
}
70+
```

0 commit comments

Comments
 (0)