Postboy Help

Middleware Lifecycle and Contract (before/after/canHandle/dispose)

Summary

Postboy middleware is a small object with a predictable lifecycle. For each bus operation (publish, callback, execute), Postboy calls middleware hooks in a fixed order:

  1. Build a pipeline context (stage + message)

  2. Ask whether the middleware should run (canHandle)

  3. Run pre-hook (before) and optionally interrupt the operation

  4. Run the operation (deliver to subscribers / execute handler)

  5. Run post-hook (after) on successful completion

  6. Eventually, release resources (dispose) when middleware is removed or the bus is disposed

This page defines the behavioral contract you can rely on when implementing middleware.

When to use / When not to use

Use this contract knowledge when you

  • Write middleware that must be safe (no leaks, no double-cleanup issues).

  • Need to reason about ordering and cancellation.

  • Want consistent logging/metrics that aligns with actual execution boundaries.

Do not rely on middleware for

  • Complex orchestration or workflow logic.

  • Guaranteed post-hooks on failures (unless explicitly documented elsewhere).

  • Framework lifecycle semantics (Angular/React/etc.). Keep core middleware framework-agnostic.

The Middleware interface (conceptual)

A Postboy middleware exposes four methods:

  • canHandle(context) → boolean
    Filter hook. If it returns false, the middleware is skipped for the current operation.

  • before(context) → decision
    Pre-hook. Returns a decision: Continue or Interrupt.

  • after(context, result?) → void
    Post-hook. Called after the operation completes successfully. May receive a result for stages that produce one.

  • dispose() → void
    Cleanup hook. Called when middleware is removed or the middleware service is disposed.

Execution order and guarantees

Per operation: call order

For each operation, Postboy iterates over the middleware list in registration order and applies the same pattern.

For each middleware M:

  1. Build context = { stage, message }

  2. If M.canHandle(context) is false → skip the middleware for this operation

  3. Call M.before(context)

  • If it returns Interrupt → the whole operation is cancelled immediately

  1. If the operation completes successfully → call M.after(context, result?)

What “interrupt” means

If any middleware returns Interrupt from before:

  • The bus does not proceed to handler execution / message delivery.

  • Postboy raises a cancellation error.

  • No later middleware in the chain is executed for that operation.

  • The after hooks are not executed for that operation (because the operation never ran).

This is the key contract: cancellation is decided strictly in the before-phase.

A lifecycle diagram (per operation)

@startuml title Middleware hooks per operation (single message)

participant "Postboy" as Bus participant "Middleware #1" as M1 participant "Middleware #2" as M2 participant "Handlers/Subscribers" as H

Bus -> M1: canHandle(context)? alt false Bus -> M2: canHandle(context)? else true Bus -> M1: before(context) -> decision alt Interrupt Bus --> Bus: cancel operation stop end end

Bus -> M2: canHandle(context)? alt true Bus -> M2: before(context) -> decision alt Interrupt Bus --> Bus: cancel operation stop end end

Bus -> H: run operation (publish/callback/execute) H --> Bus: result? (stage-dependent)

Bus -> M1: after(context, result?) [only if canHandle was true] Bus -> M2: after(context, result?) [only if canHandle was true]

@enduml

Method-by-method contract

1) canHandle(context)

Purpose: fast, explicit filtering.

Expected characteristics:

  • Should be cheap (called for every middleware for every operation).

  • Should not have side effects.

  • Should rely on information available in context:

    • stage

    • message (including message type/metadata your app defines)

Common usage patterns:

  • Stage filter: run only for execute-stage, or only for publish-stage.

  • Message filter: run only for a subset of message types.

  • Environment filter: disable in tests or enable only in production.

Pitfall: returning true for everything leads to noisy logs and unnecessary overhead.

2) before(context)

Purpose: pre-execution hook and the only place where you can block the operation.

What it is good for:

  • Policy checks (authorization/feature flags/maintenance mode).

  • Correlation/tracing setup (start span, attach correlation id to a context you own).

  • Timing start (store start timestamp in middleware state keyed by message id).

Rules of thumb:

  • Keep it deterministic and quick.

  • If it may throw, document it and treat it as a failure mode (users will see an exception).

Interrupt semantics:

  • Returning Interrupt cancels immediately.

  • Do not assume any after will run after an interrupt.

3) after(context, result?)

Purpose: post-execution hook for observation and cleanup of per-operation bookkeeping.

When it is called:

  • Only if:

    • canHandle returned true, and

    • the operation completed successfully (i.e., it reached the post-hook phase)

Result parameter:

  • Stage-dependent:

    • Stages that have a meaningful “result” may provide it here.

    • For stages without a result, it may be absent/undefined.

Good uses:

  • Stop timing and record duration.

  • Log outcome (success) and relevant metadata.

  • Release per-operation resources stored in middleware state.

Important limitation:

  • Do not rely on after to always run (cancellation or failures may skip it). If you need hard guarantees, design your middleware state management accordingly.

4) dispose()

Purpose: release long-lived resources owned by the middleware instance.

Typical things to clean up:

  • Subscriptions created by the middleware (if any).

  • Timers/intervals.

  • External handles (sockets, file descriptors).

  • In-memory maps used for timing/correlation (if they can grow unbounded).

When it is called:

  • When a middleware is explicitly removed from the chain.

  • When the middleware service is disposed (e.g., bus teardown).

Best practice:

  • Make dispose idempotent (safe to call once; avoid throwing).

State management guidance (safe patterns)

Prefer per-message keys for temporary state

If you store temporary state between before and after (like timing), key it by a stable identifier:

  • message id (or equivalent)

  • and optionally stage

Always handle the “no after()” case

Because cancellation can skip after:

  • Use bounded structures (LRU / TTL) for maps if you worry about leaks.

  • Or avoid storing state unless necessary.

Pitfalls / Troubleshooting

“My after() is not called”

Most common causes:

  1. A middleware earlier in the chain interrupted in before.

  2. The operation failed before reaching the post-hook phase.

  3. canHandle returned false for that middleware.

How to diagnose:

  • Add a minimal log in before and after and include stage + message id.

  • Temporarily narrow the middleware chain to isolate which middleware cancels.

“Middleware runs for everything and slows down the app”

Cause: canHandle always returns true and before does heavy work. Fix:

  • Add stage filtering.

  • Add message-type filtering.

  • Keep canHandle cheap and push heavy work into after only when needed.

“dispose() is never called”

Cause: middleware is never removed and the bus is never disposed in the app lifecycle. Fix:

  • Ensure you have a predictable teardown point (app shutdown, test cleanup).

  • In tests, explicitly dispose the bus or use a harness that does.

Best practices (checklist)

  • Use canHandle as your primary “scope” tool (stage + message filters).

  • Keep before fast and safe; interrupt only for clear policy reasons.

  • Use after for observation and for clearing per-operation state.

  • Implement dispose for long-lived resource cleanup; make it safe and idempotent.

  • Document ordering assumptions if your middleware depends on other middleware.

Next steps

  • [Middleware stages: Publish / Callback / Execute]

  • [Interrupt/cancel semantics and error diagnostics]

  • [How-to: filter middleware to specific messages]

  • [How-to: logging/tracing middleware]

  • [Middleware API reference]

29 июня 2026