Interrupt / Cancel: How Middleware Stops an Operation
Summary
Postboy middleware can stop (cancel) an operation before it reaches handlers/subscribers. This happens when a middleware returns an Interrupt decision from its before(context) hook.
Cancellation is a deliberate control-flow mechanism designed for:
policy enforcement (guards),
safety rails (read-only mode),
feature flags,
environment constraints.
This page explains what “interrupt” means, what guarantees you get, and how to diagnose cancellations.
When to use / When not to use
Use interrupt/cancel when you need
Hard policies: “this operation must not run”.
Safety: block dangerous executors or requests in specific environments.
Fail-fast behavior: reject early rather than partially executing work.
Central enforcement for many message types.
Avoid interrupt/cancel when you need
Business validation that should produce a domain-level error/result. Prefer handler-level validation where possible.
Soft rules (“warn but allow”). Use logging/metrics middleware instead.
Control flow that should be explicit in application code. Hidden cancellation can be surprising if overused.
Core idea: cancellation is decided in the before-phase
Middleware runs in two phases per operation:
before: decide whether the operation may proceedafter: observe a successful completion
Interrupt happens only in before. If any middleware interrupts, Postboy cancels immediately and does not execute the underlying operation.
Practical meaning:
no handler/subscriber sees the message,
later middleware in the chain is not executed for that operation,
post-hooks are not executed for that operation.
What exactly happens on interrupt
High-level behavior
When middleware returns Interrupt:
Postboy aborts the current operation.
Postboy throws a cancellation error (or completes the operation with an error, depending on the API surface).
The cancellation includes diagnostic metadata such as:
stage
message identifier
which middleware cancelled
a human-readable reason
Sequence diagram (success vs cancel)
@startuml title Middleware interrupt vs normal completion
actor User participant "Postboy" as Bus participant "Middleware chain" as MW participant "Handlers/Subscribers" as H
== Normal == User -> Bus: start operation Bus -> MW: before(stage, message) MW --> Bus: Continue Bus -> H: run operation H --> Bus: success (result?) Bus -> MW: after(stage, message, result?) Bus --> User: completed
== Interrupted == User -> Bus: start operation Bus -> MW: before(stage, message) MW --> Bus: Interrupt Bus --> User: cancelled (error)
@enduml
Cancellation is “all-or-nothing” at the bus boundary
Interrupt is not “stop one subscriber” or “skip one handler”. It is “stop the whole operation before it starts”.
This property is valuable because:
it prevents partial effects,
it keeps policies centralized,
it avoids inconsistent cross-component state.
How cancellation differs by stage
Publish stage
Cancelling Publish means: the message is not broadcast.
Use sparingly. Events are often “facts”; blocking them can hide important signals.
Prefer guarding the cause (command/callback/executor) rather than blocking the event.
Callback stage
Cancelling Callback means: the request is not delivered to subscribers.
The caller receives a cancellation error in the callback flow.
This is appropriate for access control and feature flags on request-like messages.
Execute stage
Cancelling Execute means: the executor is not executed.
The caller does not get a result; instead it receives a cancellation error.
This is the most common place for strict guards.
Ordering rules (why “who cancels” matters)
Middleware is evaluated in the order it is registered.
Consequences:
The first middleware that interrupts determines the cancellation metadata (“cancelled by X”).
Middlewares later in the chain will not run for that operation.
If you have “must-run” logging/metrics, be explicit about whether it should run:
before-phase logging can still run until the cancelling middleware is reached
after-phase logging will not run on cancellations
Recommended approach:
Put guard/policy middleware early.
Put observability middleware either:
early (to log attempts, including cancellations), or
late (to log only successful operations)
Document the intended ordering if it matters.
Diagnostics: what you should capture on cancel
To make cancellations actionable, record:
stage (Publish/Callback/Execute)
message id (or equivalent stable identifier)
middleware name (who cancelled)
reason (human-readable)
If you implement a guard middleware, treat “reason” as part of your public contract:
keep it stable enough to search in logs,
avoid leaking secrets (do not include tokens, PII, etc.),
make it specific (e.g., “Readonly mode: Execute blocked” is better than “Forbidden”).
Troubleshooting
Symptom: “My handler is never called”
Most likely causes:
A middleware interrupts in
beforefor this stage/message.Your middleware filtering is too broad (
canHandlereturns true unexpectedly).You are invoking a different stage than you think (Publish vs Callback vs Execute).
Fix steps:
Temporarily add attempt logging in
beforefor relevant middleware:
stage
message id
decision (Continue/Interrupt)
Narrow
canHandleby stage first, then by message types.Verify which Postboy API path is used (which stage is triggered).
Symptom: “Cancellation is random / inconsistent”
Likely causes:
multiple guard middlewares with overlapping conditions,
order dependency not documented,
conditions depend on mutable global state.
Fix steps:
Make guard conditions deterministic and explicit.
Consolidate policies into one middleware when possible, or clearly document precedence.
Add structured logs showing which middleware evaluated and what it decided.
Symptom: “I need cleanup but after() doesn’t run on cancel”
That is expected: interrupt stops the operation before it runs, so after is not guaranteed.
Fix:
Avoid allocating per-operation resources in
beforethat requireafterfor cleanup.If you must allocate, use bounded state and fallbacks (TTL/LRU) so cancellation cannot leak memory.
Best practices
Treat cancellation as a policy tool, not a general control-flow mechanism.
Prefer stage-first filtering to avoid accidental cancellation of unrelated operations.
Keep guard middleware:
fast,
deterministic,
transparent (clear diagnostic reason).
Avoid blocking Publish unless you have a strong architectural reason.
Decide whether your logging should cover:
attempts (including cancelled), or
only successful completions, and place middleware accordingly.
Next steps
[Middleware stages: Publish / Callback / Execute][How-to: implement a guard/policy middleware][How-to: logging/tracing middleware that includes cancellations][Troubleshooting: diagnosing middleware ordering and unexpected cancels][Middleware API reference: decisions and cancellation error fields]