Skip to content

fix(middleware): circuit-breaker state is shared across mounted drivers, so one failing provider blocks a healthy one #104

Description

@productdevbook

What happens

withCircuitBreaker() keeps one state/failures pair in the closure returned by the factory. A middleware registered with email.use() wraps every mounted driver, so one failing provider opens the circuit for all of them.

Reproduction

const healthy = mock()
const email = createEmail({
  driver: mock({ fail: true }),          // the provider that is down
  defaults: { from: "f@x.com" },
  use: [withCircuitBreaker({ threshold: 2, now: () => 0 })],
})
email.mount("other", healthy)            // a completely different provider

await email.send(msg)                    // fails
await email.send(msg)                    // fails -> circuit opens
await email.send({ ...msg, stream: "other" })

Observed:

healthy mount: BLOCKED (NETWORK: [unemail] [mock] circuit is open — provider is failing)
healthy inbox size: 0   (expected 1)

Why it matters

mount() exists so a message can be routed to a different provider — the failover story the README sells. The breaker defeats exactly that: Resend going down stops the SES mount from sending, and the error blames the wrong driver. The outage gets wider instead of narrower.

Where

src/middleware/circuit-breaker.ts:39-42 — state, failures and openedAt are per middleware instance, not per destination. ctx.driver is available on every call and is not consulted.

Suggested fix

Key the state by destination (ctx.driver plus ctx.stream) in a Map, so each provider trips independently. onStateChange already takes the driver name, which suggests per-driver was the intent. Alternatively document that the breaker belongs on wrap(driver, ...) rather than use() — but the default should be the safe one.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions