Skip to content

Middleware ​

Middleware wraps command execution in Klix. It is where you put behavior that should happen around handlers instead of inside them.

See also:

Why Middleware Exists ​

Handlers should usually focus on one command's job. Middleware exists for concerns like:

  • logging
  • timing
  • authorization
  • request shaping
  • common error-related behavior

That keeps handlers smaller and easier to reuse.

The Middleware Context ​

Current middleware receives:

  • ctx.raw_input
  • ctx.session
  • ctx.command
  • ctx.cancelled

The current implementation parses commands before middleware runs, so ctx.command is available for command-aware decisions.

Basic Example ​

python
import time

@app.middleware
async def timing(ctx: klix.MiddlewareContext, next: klix.NextFn):
    start = time.time()
    await next(ctx)
    ctx.session.ui.print(f"{time.time() - start:.2f}s", color="muted", dim=True)

Short-Circuiting ​

Middleware can stop dispatch by not calling next():

python
@app.middleware
async def auth(ctx: klix.MiddlewareContext, next: klix.NextFn):
    if ctx.command and ctx.command.name == "/deploy" and not ctx.session.state.authenticated:
        ctx.session.ui.print("Authentication required.", color="error")
        ctx.cancelled = True
        return

    await next(ctx)

Order ​

Middleware runs in registration order:

python
@app.middleware
async def first(ctx, next):
    print("first before")
    await next(ctx)
    print("first after")

@app.middleware
async def second(ctx, next):
    print("second before")
    await next(ctx)
    print("second after")

Flow:

text
first before
second before
handler
second after
first after

The chain is built in reverse internally so the final execution order matches registration order.

Middleware vs Events ​

Use middleware when:

  • you need to wrap command execution
  • you need access to parsed command data
  • you want before/after behavior around handlers

Use events when:

  • you want lifecycle hooks
  • you want general notification behavior
  • you do not need to wrap the dispatch flow itself

See Events.

Realistic Example ​

python
@app.middleware
async def status_updates(ctx: klix.MiddlewareContext, next: klix.NextFn):
    if ctx.command:
        ctx.session.ui.layout.status.set(
            left="Running",
            right=ctx.command.name,
            color="muted",
        )
        ctx.session.ui.layout.redraw_ui()

    await next(ctx)

    ctx.session.ui.layout.status.set("Ready", "idle", color="muted")
    ctx.session.ui.layout.redraw_ui()

Pitfalls ​

Mutating too much shared state ​

Middleware is powerful, so it is easy to make command behavior hard to follow by hiding too much logic there.

Forgetting await next(ctx) ​

If you intended wrapping behavior and forget to call next(), the handler will never execute.

Expecting middleware for custom plain-text loops ​

If you build your own input loop outside App.run(), you must explicitly build and call the middleware chain yourself if you want the same behavior.

Released under the MIT License.