Middleware
Middleware is a typed interception layer that runs around every fire, fireCallback, and exec call on the bus. Use it for cross-cutting concerns — logging, tracing, metrics, validation, and access control — so that handlers and executors stay focused on business behavior. Middleware can observe every operation and cancel it before it runs.
The contract
Middleware extends the abstract PostboyMiddleware class and overrides its hooks.
Member | Purpose | Default |
|---|---|---|
| Identifies the middleware in cancellation details | class name; override via |
| Filter: |
|
| Pre-hook; the only place that can cancel the operation |
|
| Post-hook; | no-op |
| Cleanup; called on removal and on bus disposal | no-op |
PipelineContext carries two fields: stage (which pipeline phase is running) and message (the fired message or executed executor). Treat it as read-only.
The three stages
Every bus operation maps to exactly one MiddlewareStage:
Stage | Runs around |
|
|---|---|---|
|
| no result |
|
| no result; the hook fires on every emitted result value |
|
| the executor's return value |
Two nuances:
fireCallbackwithout anactiondispatches lazily:beforeruns at call time, delivery happens when the returnedObservableis subscribed.Infrastructure messages are executors, so they pass through the
Executestage like any application command.
What runs when
For every operation the bus walks the chain in registration order:
canHandle(context)— middleware returningfalseis skipped entirely.before(context)— the firstInterruptdecision cancels the operation immediately.The operation runs: subscribers are notified, or the executor is invoked.
after(context, result?)— for each middleware whosecanHandlematched.
dispose() is lifecycle-level, not per-operation. See Cancellation for what an interrupt does.
Adding and removing middleware
Middleware is managed with infrastructure messages, like every other bus mutation:
The
postboy.addMiddleware(...)andpostboy.removeMiddleware(...)service methods were removed in 3.5. The messages are the only way to mutate the chain.The chain keeps insertion order. Adding the same instance twice runs its hooks twice.
postboy.dispose()callsdispose()on every registered middleware.
Filtering with canHandle
context.message.id is the static ID of the message class — the same key the bus routes by. It is stable per message type, which makes it the primary filter alongside stage.
Infrastructure messages travel through the Execute stage with their own IDs. Prefix your application IDs, or exclude the known infrastructure IDs, when middleware must not touch them:
Pitfalls
canHandleruns for every middleware on every operation. Keep it cheap and side-effect free.afteris not guaranteed. It is skipped when any middleware interrupts. Put mandatory cleanup indispose(), not inafter.A locked message type is silently not delivered, but its middleware hooks still run as if the operation succeeded — middleware cannot tell a locked dispatch from a delivered one.