Postboy Help

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:

  1. before: decide whether the operation may proceed

  2. after: 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:

  1. A middleware interrupts in before for this stage/message.

  2. Your middleware filtering is too broad (canHandle returns true unexpectedly).

  3. You are invoking a different stage than you think (Publish vs Callback vs Execute).

Fix steps:

  1. Temporarily add attempt logging in before for relevant middleware:

  • stage

  • message id

  • decision (Continue/Interrupt)

  1. Narrow canHandle by stage first, then by message types.

  2. 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 before that require after for 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]

29 июня 2026