diff --git a/en/guide/interceptors.md b/en/guide/interceptors.md index 95898e9..68f92c4 100644 --- a/en/guide/interceptors.md +++ b/en/guide/interceptors.md @@ -16,9 +16,10 @@ import routes from '#gen/routes.js'; const server = createServer({ services: [routes], port: 5000, + // errorHandler + validation by default; resilience is opt-in interceptors: createDefaultInterceptors({ - timeout: { duration: 10_000 }, - retry: { maxRetries: 5 }, + timeout: { duration: 10_000 }, // explicitly enabled + retry: { maxRetries: 5 }, // explicitly enabled }), shutdown: { autoShutdown: true }, }); @@ -39,18 +40,20 @@ Response <- interceptor1 <- interceptor2 <- ... <- handler ### Built-in Chain -`createDefaultInterceptors()` provides 8 production-ready interceptors in a fixed order: +`createDefaultInterceptors()` is a chain factory for 8 production-ready interceptors in a fixed order: | # | Interceptor | Purpose | Default | |---|-------------|---------|---------| | 1 | **errorHandler** | Normalizes errors into ConnectError | Enabled | -| 2 | **timeout** | Limits request execution time | Enabled (30s) | -| 3 | **bulkhead** | Limits concurrent requests | Enabled (capacity 10, queue 10) | -| 4 | **circuitBreaker** | Prevents cascading failures | Enabled (threshold 5) | -| 5 | **retry** | Retries transient failures with exponential backoff | Enabled (3 retries) | -| 6 | **fallback** | Graceful degradation | Disabled | +| 2 | **timeout** | Limits request execution time | **Opt-in** (30s when enabled) | +| 3 | **bulkhead** | Limits concurrent requests | **Opt-in** (capacity 10, queue 10 when enabled) | +| 4 | **circuitBreaker** | Prevents cascading failures (outbound pattern) | **Opt-in** (threshold 5 when enabled) | +| 5 | **retry** | Retries transient failures with exponential backoff | **Opt-in** (3 retries when enabled) | +| 6 | **fallback** | Graceful degradation | **Opt-in** (requires a handler) | | 7 | **validation** | Validates via @connectrpc/validate | Enabled | -| 8 | **serializer** | JSON serialization for protobuf | **Disabled** | +| 8 | **serializer** | JSON serialization for protobuf | **Opt-in** | + +**No hidden behavioral logic.** Only structural interceptors (errorHandler, validation) are enabled by default. Resilience interceptors (timeout, bulkhead, circuitBreaker, retry) alter request behavior and must be enabled explicitly with `true` or an options object. ### Per-Method Routing diff --git a/en/guide/interceptors/built-in.md b/en/guide/interceptors/built-in.md index e585b02..9787178 100644 --- a/en/guide/interceptors/built-in.md +++ b/en/guide/interceptors/built-in.md @@ -4,7 +4,7 @@ outline: deep # Built-in Interceptors -Connectum provides 8 production-ready interceptors via `createDefaultInterceptors()`. They form a fixed chain that covers error handling, resilience, validation, and serialization. +Connectum provides 8 production-ready interceptors via `createDefaultInterceptors()`. They form a fixed-order chain that covers error handling, resilience, validation, and serialization. ## The Default Chain @@ -15,15 +15,54 @@ errorHandler -> timeout -> bulkhead -> circuitBreaker -> retry -> fallback -> va | # | Interceptor | Purpose | Default | |---|-------------|---------|---------| | 1 | **errorHandler** | Normalizes errors into `ConnectError` | Enabled | -| 2 | **timeout** | Limits request execution time | Enabled (30s) | -| 3 | **bulkhead** | Limits concurrent requests | Enabled (capacity 10, queue 10) | -| 4 | **circuitBreaker** | Prevents cascading failures | Enabled (threshold 5) | -| 5 | **retry** | Retries transient failures with exponential backoff | Enabled (3 retries) | -| 6 | **fallback** | Graceful degradation | Disabled | +| 2 | **timeout** | Limits request execution time | **Opt-in** (30s when enabled) | +| 3 | **bulkhead** | Limits concurrent requests | **Opt-in** (capacity 10, queue 10 when enabled) | +| 4 | **circuitBreaker** | Prevents cascading failures (outbound pattern, see below) | **Opt-in** (threshold 5 when enabled) | +| 5 | **retry** | Retries transient failures with exponential backoff | **Opt-in** (3 retries when enabled) | +| 6 | **fallback** | Graceful degradation | **Opt-in** (requires a handler) | | 7 | **validation** | Validates via `@connectrpc/validate` | Enabled | -| 8 | **serializer** | JSON serialization for protobuf | **Disabled** | +| 8 | **serializer** | JSON serialization for protobuf | **Opt-in** | -The order is deliberate: `errorHandler` is outermost (catches everything), `serializer` is innermost (closest to the handler). +The order is deliberate: `errorHandler` is outermost (catches everything), `serializer` is innermost (closest to the handler). The order applies to whichever interceptors you enable. In particular, `circuitBreaker` wraps `retry`, so one logical request increments the failure counter at most once regardless of retry attempts. + +::: warning No hidden behavioral logic +Only structural interceptors (errorHandler, validation) are enabled by default. Resilience interceptors (timeout, bulkhead, circuitBreaker, retry) alter request behavior and must be enabled explicitly with `true` or an options object — implicitly enabled resilience caused a confirmed production incident (a server-side circuit breaker tripped by expected business errors). +::: + +## Circuit Breaker: Placement and Error Classification + +The circuit breaker is an **outbound/client-side pattern**: it protects the caller from a sick upstream (fail fast instead of waiting on timeouts) and gives that upstream room to recover. On a server's inbound stack it degenerates into error-rate load shedding — for inbound protection prefer explicit `timeout` + `bulkhead`. + +```typescript +// Recommended: circuit breaker on an outbound client transport +import { createConnectTransport } from '@connectrpc/connect-node'; +import { createCircuitBreakerInterceptor } from '@connectum/interceptors'; + +const transport = createConnectTransport({ + baseUrl: 'http://upstream:5000', + interceptors: [ + createCircuitBreakerInterceptor({ threshold: 5, halfOpenAfter: 30_000 }), + ], +}); +``` + +**Error classification.** By default only infrastructure errors count as circuit failures: `Unknown`, `DeadlineExceeded`, `Internal`, `Unavailable`, `DataLoss`, `ResourceExhausted` (plus any non-`ConnectError` thrown value). Business codes (`invalid_argument`, `not_found`, `failed_precondition`, `already_exists`, ...) are expected responses of a healthy service: they never open the breaker, and in half-open state they close it. + +Customize with `failurePredicate(error, defaultPredicate)` — the default predicate (exported as `defaultFailurePredicate`) is passed in for composition: + +```typescript +import { Code, ConnectError } from '@connectrpc/connect'; +import { createCircuitBreakerInterceptor } from '@connectum/interceptors'; + +// Exclude upstream per-client rate limits from tripping the breaker +createCircuitBreakerInterceptor({ + failurePredicate: (err, def) => + def(err) && !(err instanceof ConnectError && err.code === Code.ResourceExhausted), +}); + +// Restore legacy behavior (every error trips the breaker) +createCircuitBreakerInterceptor({ failurePredicate: () => true }); +``` ::: tip When to enable the serializer Enable the serializer when your service uses the **Connect protocol** (HTTP/1.1 JSON) and you need automatic protobuf ↔ JSON conversion. Not needed for pure **gRPC** services (binary protobuf format). @@ -74,16 +113,15 @@ await server.start(); ## Customizing the Default Chain -Pass options to `createDefaultInterceptors()` to customize individual interceptors. Set an interceptor to `false` to disable it entirely: +Pass options to `createDefaultInterceptors()` to customize individual interceptors. Pass `true` or an options object to enable an opt-in interceptor; set one of the default-enabled interceptors to `false` to disable it: ```typescript import { createDefaultInterceptors } from '@connectum/interceptors'; const interceptors = createDefaultInterceptors({ - timeout: { duration: 10_000 }, // Custom timeout (10s instead of 30s) - retry: false, // Disable retry - bulkhead: { capacity: 20, queueSize: 20 }, // Higher concurrency limits - // All others remain at defaults + timeout: { duration: 10_000 }, // Enable timeout (10s) + bulkhead: { capacity: 20, queueSize: 20 }, // Enable bulkhead with custom limits + // errorHandler and validation remain enabled by default }); const server = createServer({ diff --git a/en/guide/production/architecture.md b/en/guide/production/architecture.md index 94f1859..2e32c74 100644 --- a/en/guide/production/architecture.md +++ b/en/guide/production/architecture.md @@ -349,7 +349,7 @@ async createOrder(req) { ### Circuit Breaker at Application Level -Connectum's built-in interceptor chain includes a circuit breaker. For inter-service calls, configure it per-client: +Connectum provides a circuit breaker interceptor (opt-in — it is not enabled by default). It is an outbound/client-side pattern: enable it per-client for inter-service calls; for server inbound protection prefer explicit `timeout` + `bulkhead`: ```typescript import { createDefaultInterceptors } from '@connectum/interceptors'; @@ -358,8 +358,8 @@ const transport = createGrpcTransport({ baseUrl: 'http://inventory-service:5000', httpVersion: '2', interceptors: createDefaultInterceptors({ - circuitBreaker: { failureThreshold: 5 }, - timeout: { timeoutMs: 5000 }, + circuitBreaker: { threshold: 5 }, + timeout: { duration: 5000 }, retry: { maxRetries: 2 }, // Disable server-side-only interceptors bulkhead: false, diff --git a/en/guide/quickstart.md b/en/guide/quickstart.md index fa8fb12..93286c6 100644 --- a/en/guide/quickstart.md +++ b/en/guide/quickstart.md @@ -254,15 +254,13 @@ curl http://localhost:5000/healthz | Feature | Details | |---------|---------| | **Error handling** | Automatic error normalization to gRPC status codes | -| **Timeout** | 30s default per request | -| **Bulkhead** | Max 10 concurrent requests + 10-item queue | -| **Circuit breaker** | Opens after 5 consecutive failures | -| **Retry** | 3 retries with exponential backoff | | **Validation** | Proto constraint validation via [@connectrpc/validate](https://github.com/connectrpc/validate-es) | | **Health checks** | gRPC + HTTP endpoints | | **Reflection** | Runtime service discovery | | **Graceful shutdown** | SIGTERM/SIGINT with connection draining | +Resilience interceptors (timeout, bulkhead, circuit breaker, retry) are **opt-in** — enable them explicitly via `createDefaultInterceptors()` options. See [Step 12](#_12-built-in-interceptors). + --- The following steps show how to extend your base service with additional framework features. @@ -377,24 +375,24 @@ See [Graceful Shutdown](/en/guide/server/graceful-shutdown) for dependency graph ## 12. Built-in Interceptors -`createDefaultInterceptors()` assembles 8 interceptors in a fixed order: +`createDefaultInterceptors()` assembles up to 8 interceptors in a fixed order. Only errorHandler and validation are enabled by default — resilience interceptors are opt-in (no hidden behavioral logic): | # | Interceptor | Default | Purpose | |---|-------------|---------|---------| | 1 | **errorHandler** | on | Normalize errors to gRPC status codes | -| 2 | **timeout** | 30s | Enforce per-request deadline | -| 3 | **bulkhead** | 10/10 | Limit concurrent requests + queue | -| 4 | **circuitBreaker** | 5 failures | Prevent cascading failures | -| 5 | **retry** | 3 retries | Exponential backoff for transients | +| 2 | **timeout** | opt-in (30s when enabled) | Enforce per-request deadline | +| 3 | **bulkhead** | opt-in (10/10 when enabled) | Limit concurrent requests + queue | +| 4 | **circuitBreaker** | opt-in (5 failures when enabled) | Prevent cascading failures (outbound pattern) | +| 5 | **retry** | opt-in (3 retries when enabled) | Exponential backoff for transients | | 6 | **fallback** | off | Graceful degradation (requires handler) | | 7 | **validation** | on | Proto constraint validation | | 8 | **serializer** | off | JSON serialization (opt-in for Connect protocol) | ```typescript +// Enable resilience interceptors explicitly (true or an options object) const interceptors = createDefaultInterceptors({ - retry: false, timeout: { duration: 10_000 }, - bulkhead: { maxConcurrent: 20, maxQueue: 50 }, + bulkhead: { capacity: 20, queueSize: 50 }, }); ``` diff --git a/en/guide/service-communication.md b/en/guide/service-communication.md index 90fa22a..32e2fd0 100644 --- a/en/guide/service-communication.md +++ b/en/guide/service-communication.md @@ -27,7 +27,7 @@ const inventoryTransport = createGrpcTransport({ serverPort: Number(process.env.INVENTORY_PORT), }), ...createDefaultInterceptors({ - circuitBreaker: { failureThreshold: 5 }, + circuitBreaker: { threshold: 5 }, timeout: { duration: 5_000 }, retry: { maxRetries: 2 }, // Disable server-only interceptors @@ -69,11 +69,11 @@ Not all server interceptors are appropriate for client transports. When using `c | Interceptor | Server | Client | Notes | |-------------|:------:|:------:|-------| | **errorHandler** | Yes | No | Normalizes errors for responses -- not needed on client | -| **timeout** | Yes | Yes | Enforce per-request deadline | -| **bulkhead** | Yes | No | Limits server concurrency -- not applicable to clients | -| **circuitBreaker** | Yes | Yes | Prevents cascading failures to downstream services | -| **retry** | Yes | Yes | Retries transient errors | -| **fallback** | Yes | No | Requires server-side handler | +| **timeout** | Opt-in | Opt-in | Enforce per-request deadline | +| **bulkhead** | Opt-in | No | Limits server concurrency -- not applicable to clients | +| **circuitBreaker** | Opt-in | Opt-in | Outbound pattern -- prevents cascading failures to downstream services | +| **retry** | Opt-in | Opt-in | Retries transient errors | +| **fallback** | Opt-in | No | Requires server-side handler | | **validation** | Yes | No | Validates incoming requests -- not outgoing | | **serializer** | Opt-in | No | Server-side JSON serialization (disabled by default; enable for Connect protocol) | diff --git a/en/guide/service-communication/client-interceptors.md b/en/guide/service-communication/client-interceptors.md index 80e82e4..ff2882e 100644 --- a/en/guide/service-communication/client-interceptors.md +++ b/en/guide/service-communication/client-interceptors.md @@ -131,7 +131,7 @@ const transport = createGrpcTransport({ serverPort: 5000, }), ...createDefaultInterceptors({ - circuitBreaker: { failureThreshold: 5 }, + circuitBreaker: { threshold: 5 }, timeout: { duration: 5_000 }, retry: { maxRetries: 2 }, // Disable server-only interceptors @@ -154,7 +154,7 @@ The circuit breaker tracks consecutive failures per client transport: | **Open** | Requests fail immediately with `Unavailable` (no downstream call) | | **Half-Open** | A single probe request is allowed; success closes, failure re-opens | -The default `failureThreshold` is 5 consecutive failures. After the circuit opens, it automatically transitions to half-open after a cooldown period. +The default `threshold` is 5 consecutive failures. After the circuit opens, it automatically transitions to half-open after a cooldown period. ### Per-Service Configuration @@ -170,7 +170,7 @@ const paymentTransport = createGrpcTransport({ ...createDefaultInterceptors({ timeout: { duration: 3_000 }, retry: { maxRetries: 3 }, - circuitBreaker: { failureThreshold: 3 }, + circuitBreaker: { threshold: 3 }, bulkhead: false, errorHandler: false, serializer: false, validation: false, }), ], @@ -185,7 +185,7 @@ const recommendationTransport = createGrpcTransport({ ...createDefaultInterceptors({ timeout: { duration: 10_000 }, retry: { maxRetries: 1 }, - circuitBreaker: { failureThreshold: 10 }, + circuitBreaker: { threshold: 10 }, bulkhead: false, errorHandler: false, serializer: false, validation: false, }), ], diff --git a/en/guide/validation.md b/en/guide/validation.md index 05e1577..619bf98 100644 --- a/en/guide/validation.md +++ b/en/guide/validation.md @@ -18,7 +18,7 @@ Client → errorHandler → ... → validation → serializer → Handler Invalid: INVALID_ARGUMENT ``` -Validation runs as the 7th interceptor in the default chain (before serializer, after resilience interceptors). Invalid requests are rejected with `INVALID_ARGUMENT` before reaching the handler. +Validation runs as the 7th interceptor in the default chain (before serializer, after any explicitly enabled resilience interceptors). Invalid requests are rejected with `INVALID_ARGUMENT` before reaching the handler. ## Setup diff --git a/en/index.md b/en/index.md index d896dae..e553dda 100644 --- a/en/index.md +++ b/en/index.md @@ -25,7 +25,7 @@ features: details: Opinionated production architecture with a single createServer() entry point. Predictable behavior across all your services — no glue code, no custom wiring. - icon: '' title: Operational Safety Envelope - details: Fixed-order interceptor chain — timeout, retry, circuit breaker, bulkhead, and fallback. Enterprise resilience patterns enforced by default. + details: Fixed-order interceptor chain — timeout, retry, circuit breaker, bulkhead, and fallback. Explicit, opt-in resilience patterns with no hidden behavioral logic. - icon: '' title: Auth & Zero-Trust details: JWT, gateway, and session authentication with declarative RBAC. mTLS and proto-based authorization keep security alongside your API contract. diff --git a/en/migration/index.md b/en/migration/index.md index 4864224..1d6eb52 100644 --- a/en/migration/index.md +++ b/en/migration/index.md @@ -7,6 +7,58 @@ description: Migration guides and breaking changes for Connectum releases This page covers breaking changes and migration steps between Connectum releases. +## BREAKING: Resilience interceptors are now opt-in in `createDefaultInterceptors()` + +> Applies to the next release on top of RC.10. + +`createDefaultInterceptors()` now enables only the structural interceptors — **errorHandler** and **validation**. The resilience interceptors (**timeout**, **bulkhead**, **circuitBreaker**, **retry**) are **opt-in**: enable each one explicitly with `true` or an options object. + +**Why**: no hidden behavioral logic. Implicitly enabled resilience caused a confirmed production incident — a server-side circuit breaker was tripped by expected business errors (such as `invalid_argument`) and started rejecting healthy traffic. + +| Interceptor | Before | After | +|-------------|--------|-------| +| errorHandler | enabled | enabled (unchanged) | +| timeout | enabled (30s) | **opt-in** (30s when enabled) | +| bulkhead | enabled (10/10) | **opt-in** (10/10 when enabled) | +| circuitBreaker | enabled (5 failures) | **opt-in** (5 failures when enabled) | +| retry | enabled (3 attempts) | **opt-in** (3 attempts when enabled) | +| fallback | disabled | opt-in (unchanged) | +| validation | enabled | enabled (unchanged) | +| serializer | disabled | opt-in (unchanged) | + +**Migration**: + +```typescript +// Before — timeout, bulkhead, circuitBreaker, retry were implicitly active +createDefaultInterceptors() + +// After — to keep the previous behavior, enable them explicitly +createDefaultInterceptors({ + timeout: true, + bulkhead: true, + circuitBreaker: true, + retry: true, +}) + +// After — if you only need errorHandler + validation (no change needed) +createDefaultInterceptors() +``` + +Code that already passes explicit options (`timeout: { duration: 10_000 }`, `retry: false`, ...) keeps working: an options object or `true` means enabled, `false` means disabled. + +### Circuit breaker: error classification and placement + +The circuit breaker now classifies errors. By default only infrastructure errors count as circuit failures: `Unknown`, `DeadlineExceeded`, `Internal`, `Unavailable`, `DataLoss`, `ResourceExhausted` (plus any non-`ConnectError` thrown value). Business codes (`invalid_argument`, `not_found`, ...) never open the breaker, and in half-open state they close it. + +Customize via the new `failurePredicate(error, defaultPredicate)` option; the default classifier is exported as `defaultFailurePredicate`: + +```typescript +// Restore legacy behavior (every error trips the breaker) +createCircuitBreakerInterceptor({ failurePredicate: () => true }); +``` + +The circuit breaker is repositioned as an **outbound/client-transport pattern**. For server inbound protection prefer explicit `timeout` + `bulkhead`. See [@connectum/interceptors](/en/packages/interceptors#circuit-breaker) for details. + ## Minimum Node.js raised to 22.13.0 > Applies to the next release on top of RC.10. @@ -381,7 +433,7 @@ await server.start(); ### Interceptors: No Auto-Defaults -Starting from v1.0.0-beta.x, `@connectum/core` has **zero internal dependencies**. Omitting the `interceptors` option (or passing `[]`) means **no interceptors are applied**. To use the default resilience chain, explicitly pass `createDefaultInterceptors()`: +Starting from v1.0.0-beta.x, `@connectum/core` has **zero internal dependencies**. Omitting the `interceptors` option (or passing `[]`) means **no interceptors are applied**. To use the default interceptor chain, explicitly pass `createDefaultInterceptors()`: ```typescript import { createServer } from '@connectum/core'; diff --git a/en/packages/interceptors.md b/en/packages/interceptors.md index 92ebf80..c3dcd12 100644 --- a/en/packages/interceptors.md +++ b/en/packages/interceptors.md @@ -33,13 +33,13 @@ pnpm add @connectum/interceptors ```typescript import { createDefaultInterceptors } from '@connectum/interceptors'; -// All defaults (fallback disabled) +// Defaults: errorHandler + validation only (resilience is opt-in) const interceptors = createDefaultInterceptors(); -// Custom configuration +// Explicitly enable resilience interceptors const interceptors = createDefaultInterceptors({ - retry: false, timeout: { duration: 10_000 }, + retry: true, circuitBreaker: { threshold: 3 }, }); ``` @@ -54,8 +54,7 @@ const server = createServer({ services: [routes], interceptors: createDefaultInterceptors({ errorHandler: { logErrors: true }, - timeout: { duration: 15_000 }, - retry: false, + timeout: { duration: 15_000 }, // explicitly enabled }), }); ``` @@ -71,13 +70,15 @@ errorHandler -> timeout -> bulkhead -> circuitBreaker -> retry -> fallback -> va | # | Interceptor | Purpose | Default | |---|-------------|---------|---------| | 1 | **errorHandler** | Catch-all error normalization (outermost) | Enabled | -| 2 | **timeout** | Enforce request deadline | Enabled (30s) | -| 3 | **bulkhead** | Limit concurrent requests | Enabled (10/10) | -| 4 | **circuitBreaker** | Prevent cascading failures | Enabled (5 failures) | -| 5 | **retry** | Retry transient failures with backoff | Enabled (3 retries) | -| 6 | **fallback** | Graceful degradation | **Disabled** | +| 2 | **timeout** | Enforce request deadline | **Opt-in** (30s when enabled) | +| 3 | **bulkhead** | Limit concurrent requests | **Opt-in** (10/10 when enabled) | +| 4 | **circuitBreaker** | Prevent cascading failures (outbound pattern) | **Opt-in** (5 failures when enabled) | +| 5 | **retry** | Retry transient failures with backoff | **Opt-in** (3 retries when enabled) | +| 6 | **fallback** | Graceful degradation | **Opt-in** (requires a handler) | | 7 | **validation** | Validate request messages (@connectrpc/validate) | Enabled | -| 8 | **serializer** | JSON serialization (innermost) | **Disabled** | +| 8 | **serializer** | JSON serialization (innermost) | **Opt-in** | + +**No hidden behavioral logic.** Only structural interceptors (errorHandler, validation) are enabled by default. Resilience interceptors (timeout, bulkhead, circuitBreaker, retry) alter request behavior and must be enabled explicitly with `true` or an options object — implicitly enabled resilience caused a confirmed production incident (a server-side circuit breaker tripped by expected business errors). ## API Reference @@ -96,13 +97,13 @@ Each interceptor can be set to `true` (defaults), `false` (disabled), or an opti ```typescript interface DefaultInterceptorOptions { errorHandler?: boolean | ErrorHandlerOptions; // default: true - timeout?: boolean | TimeoutOptions; // default: true - bulkhead?: boolean | BulkheadOptions; // default: true - circuitBreaker?: boolean | CircuitBreakerOptions; // default: true - retry?: boolean | RetryOptions; // default: true - fallback?: boolean | FallbackOptions; // default: false + timeout?: boolean | TimeoutOptions; // default: false (opt-in) + bulkhead?: boolean | BulkheadOptions; // default: false (opt-in) + circuitBreaker?: boolean | CircuitBreakerOptions; // default: false (opt-in) + retry?: boolean | RetryOptions; // default: false (opt-in) + fallback?: boolean | FallbackOptions; // default: false (opt-in) validation?: boolean; // default: true - serializer?: boolean | SerializerOptions; // default: false + serializer?: boolean | SerializerOptions; // default: false (opt-in) } ``` @@ -169,6 +170,8 @@ const interceptor = createBulkheadInterceptor({ Prevents cascading failures by breaking the circuit on consecutive errors. +The circuit breaker is an **outbound/client-side pattern**: it protects the caller from a sick upstream and gives that upstream room to recover. On a server's inbound stack it degenerates into error-rate load shedding — for inbound protection prefer explicit `timeout` + `bulkhead`. When enabled in the default chain, the breaker wraps `retry`, so one logical request increments the failure counter at most once regardless of retry attempts. + ```typescript import { createCircuitBreakerInterceptor } from '@connectum/interceptors'; @@ -184,6 +187,25 @@ const interceptor = createCircuitBreakerInterceptor({ | `threshold` | `number` | `5` | Consecutive failures before opening | | `halfOpenAfter` | `number` | `30000` | Milliseconds before half-open attempt | | `skipStreaming` | `boolean` | `true` | Skip for streaming calls | +| `failurePredicate` | `(error: unknown, defaultPredicate: (error: unknown) => boolean) => boolean` | `defaultFailurePredicate` | Decides which errors count as circuit failures; receives the default predicate for composition | + +**Error classification.** By default only infrastructure errors count as circuit failures: `Unknown`, `DeadlineExceeded`, `Internal`, `Unavailable`, `DataLoss`, `ResourceExhausted` (plus any non-`ConnectError` thrown value). Business codes (`invalid_argument`, `not_found`, `failed_precondition`, `already_exists`, ...) never open the breaker, and in half-open state they close it (the upstream answered — it is alive). The default classifier is exported as `defaultFailurePredicate`. + +```typescript +import { Code, ConnectError } from '@connectrpc/connect'; +import { createCircuitBreakerInterceptor } from '@connectum/interceptors'; + +// Exclude upstream per-client rate limits from tripping the breaker +createCircuitBreakerInterceptor({ + failurePredicate: (err, def) => + def(err) && !(err instanceof ConnectError && err.code === Code.ResourceExhausted), +}); + +// Restore legacy behavior (every error trips the breaker) +createCircuitBreakerInterceptor({ failurePredicate: () => true }); +``` + +A predicate that throws is fail-closed: the error counts as a failure and the original upstream error is propagated to the caller. ### Retry @@ -320,6 +342,7 @@ type MethodFilterMap = Record; | `createSerializerInterceptor` | JSON serialization | | `createRetryInterceptor` | Retry with backoff | | `createCircuitBreakerInterceptor` | Circuit breaker | +| `defaultFailurePredicate` | Default circuit breaker error classifier (infrastructure codes only) | | `createTimeoutInterceptor` | Request timeout | | `createBulkheadInterceptor` | Concurrency limiter | | `createFallbackInterceptor` | Graceful degradation |