Middleware Troubleshooting
Summary
This page helps you diagnose common middleware issues:
middleware not running,
running too often,
cancelling operations unexpectedly,
missing
afterhooks,ordering problems,
leaks and test pollution.
It follows a “symptom → likely causes → fixes → prevention” structure.
Quick triage checklist (60 seconds)
Before deep debugging, answer these questions:
Which stage is affected? Publish, Callback, or Execute.
Is the operation cancelled? Look for a cancellation error and its metadata (stage, middleware name, message id).
Does
canHandlematch? If it returns false, the middleware is skipped.Is middleware attached to the same bus instance? Multiple buses are a common cause.
Does ordering matter? The first Interrupt wins; later middleware won’t run.
Symptom 1: “My middleware never runs”
Likely causes (ranked)
Middleware is attached to a different Postboy instance than the one used to publish/execute.
canHandlereturns false for the tested stage/message.Middleware was attached too late (after operations already happened).
You are testing a different stage than you think (Publish vs Callback vs Execute).
Fix steps
Confirm the exact bus instance used by the caller and the middleware attachment code.
Temporarily make
canHandlereturn true and verifybeforeexecutes.Add a temporary log in
beforethat prints:
stage
message label/type
message id
Attach middleware at a stable startup point (before message processing begins).
Prevention
Centralize bus creation and middleware attachment in one place.
Keep stage-first filtering and log stage during initial rollout.
Symptom 2: “Middleware runs for everything and creates noise”
Likely causes
canHandleis too broad (always true).Stage filtering is missing.
Message filtering is missing or implemented inside
beforeinstead ofcanHandle.
Fix steps
Move all scoping into
canHandle.Filter by stage first, then by message subset.
If this is a debug middleware, add an explicit feature flag to disable it.
Prevention
Adopt a “default deny” mindset for observability middleware: opt into specific stages/types.
Symptom 3: “Operations are cancelled unexpectedly”
Likely causes (ranked)
A guard middleware returns Interrupt for more cases than intended.
Middleware ordering changed; a different middleware now interrupts first.
A
canHandlepredicate matches unintended messages/stages.Guard decisions depend on mutable global state (feature flags/config) that changes during runtime.
Fix steps
Check the cancellation error metadata:
which middleware cancelled
stage
message id
Temporarily add “decision logs” to guard middleware:
stage
message label
decision (Continue/Interrupt)
policy reason / policy id
Narrow
canHandle:
stage-first
explicit message subset
Verify middleware ordering and document precedence.
Prevention
Use one-policy-per-middleware.
Keep cancellation reasons stable and searchable.
Treat guard middleware as part of your “public behavior” and test it.
Symptom 4: “My after() is not called”
Most common cause
The operation did not reach the successful post phase:
it was cancelled (Interrupt in
before), orit failed before reaching the post hook boundary.
Fix steps
If you need to observe cancellations, log/measure in
before(attempt logs).Do not rely on
afterfor critical cleanup.For timing, use bounded state so missing after does not leak memory.
Prevention
Design middleware with the assumption “after may not run”.
Put cleanup for long-lived resources in
dispose.
Symptom 5: “My logging/metrics doesn’t show cancelled attempts”
Likely causes
You only log/record in
after.Observability middleware is placed after the guard middleware in the chain.
Fix steps
Log “attempt” in
before.If you want to log cancellations, place logging middleware before guards (ordering).
Alternatively, instrument cancellation where it is handled at the call boundary.
Prevention
Decide explicitly whether you want:
attempts (including cancelled), or
only successful completions, and position middleware accordingly.
Symptom 6: “Changing middleware order changes behavior”
Likely causes
Hidden ordering dependencies.
Multiple guards overlap; precedence was accidental.
Fix steps
List the middleware chain in order and state its intent.
Identify which middleware can Interrupt.
Ensure the most specific policy runs first (or document your precedence rule).
Prevention
Avoid coupling between middleware instances.
Document order-sensitive chains in one module and treat it as configuration.
Symptom 7: “Performance regressed after adding middleware”
Likely causes (ranked)
Overbroad
canHandlecausing hot-path overhead.Heavy work in
before(serialization, logging payloads).Too many metrics labels or dynamic label building.
Synchronous logging/export on hot paths.
Fix steps
Filter by stage and message subset (tighten
canHandle).Remove payload logging; log only stable identifiers.
Precompute stable labels.
Measure overhead by comparing with and without middleware in a controlled benchmark.
Prevention
Treat middleware as part of the performance-critical path.
Keep
beforelightweight and deterministic.
Symptom 8: “Memory grows over time” (suspected leak)
Likely causes
Unbounded per-operation state (e.g., Map keyed by message id) cleared only in
after.Subscriptions created by middleware are never unsubscribed.
Timers/intervals never cleared.
Middleware attached repeatedly (multiple times) without removal/disposal.
Fix steps
Audit resources owned by middleware:
subscriptions
timers
maps/caches
Ensure all long-lived cleanup is in
dispose.Make per-operation maps bounded (TTL/LRU) and avoid relying on
after.Ensure middleware is attached once per intended lifecycle scope.
Prevention
Add automated tests that:
attach middleware,
run operations,
dispose middleware/bus,
assert resources are released.
Symptom 9: “Tests affect each other” (cross-test pollution)
Likely causes
Global bus instance reused without reset.
Middleware attached in one test and not removed/disposed.
Timers/subscriptions keep running after test completion.
Fix steps
Attach middleware per test and remove/dispose in teardown.
Dispose the bus (or a test harness that disposes it) after each test.
Ensure
disposeclears intervals and unsubscribes.
Prevention
Adopt a strict Arrange/Act/Assert + teardown pattern.
Prefer test-owned middleware scope unless you intentionally want global instrumentation.
Diagnostic flow diagram (how to localize the issue)
@startuml title Middleware debugging flow (localization)
start :Identify stage (Publish/Callback/Execute); :Check if operation was cancelled; if (Cancelled?) then (yes) :Inspect cancellation metadata\n(middleware name, reason, message id); :Check guard middleware canHandle conditions; :Verify middleware ordering; else (no) :Check whether middleware runs at all; if (Middleware runs?) then (yes) :Check canHandle scoping (too broad/too narrow); :Check before vs after expectations; else (no) :Verify middleware attachment to the correct bus; :Temporarily widen canHandle to true; :Attach earlier in lifecycle; endif endif stop
@enduml
Prevention checklist (keep this near your middleware chain)
canHandleis stage-first and cheap.Guards have stable names and reasons.
“Attempt vs success” signals are intentional.
No cleanup depends on
after.disposereleases all long-lived resources.Middleware ordering is documented if it impacts behavior.
Tests attach and dispose middleware predictably.
Next steps
[Middleware lifecycle and contract][Decision & Cancellation reference][Best practices][Anti-patterns][Recipes]