Wiki
猫Middleware
Russian version: Middleware-RU
Middleware lets you run logic before commands, payloads, and non-command update handlers. It is the main place for cross-cutting concerns such as access checks, request logging, feature flags, and lightweight context preparation.
What middleware can do
Middleware is useful when the same check or side effect should apply in more than one handler.
Typical uses:
- reject updates from unauthorized users;
- log incoming commands and callback payloads;
- attach derived values to
MessageContext; - stop processing early when a precondition is not met;
- run non-blocking side effects such as analytics or audit logging.
For the handler and plugin model around middleware, see Commands-and-Plugins. For the fields you can read or update on the context, see MessageContext.
Middleware levels
Laniakea has three middleware layers:
- bot-level middleware, added with
Bot.AddMiddleware(...); - plugin-level middleware, added with
Plugin.AddMiddleware(...); - command or payload middleware, added with
Command.Use(...).
Each level is useful for a different scope:
- bot-level middleware applies to every update the bot processes;
- plugin-level middleware applies to every handler in a single plugin;
- command-level middleware applies only to one command or payload.
Execution order
The effective order is:
- bot-level middleware;
- plugin-level middleware for the matched plugin;
- command-level middleware for the matched command or payload;
- the final handler.
For non-command update handlers registered through AddUpdateHandler(...), the flow is:
- bot-level middleware;
- plugin-level middleware for each plugin that handles that update type;
- the update handler itself.
Important detail:
- bot-level middleware runs once per update before routing;
- for non-command update handlers, each matching plugin receives its own cloned
MessageContext, so one plugin's mutations do not leak into the next plugin's handler chain.
Synchronous middleware
By default, middleware is synchronous.
The executor signature is:
func(ctx *laniakea.MessageContext, db T) bool
Return values mean:
true: continue processing;false: stop the current chain immediately.
This makes synchronous middleware the right choice for:
- authorization;
- validation;
- rate limiting gates;
- any logic that must block handler execution on failure.
Example:
auth := laniakea.NewMiddleware("auth", func(ctx *laniakea.MessageContext, db *App) bool {
if ctx.From == nil || !db.Allowed(ctx.From.ID) {
ctx.Answer("Access denied")
return false
}
return true
})
Asynchronous middleware
Middleware can also run asynchronously with SetAsync(true).
audit := laniakea.NewMiddleware("audit", func(ctx *laniakea.MessageContext, db *App) bool {
db.Audit(ctx.Update.UpdateID, ctx.Text)
return true
}).SetAsync(true)
Async middleware behaves differently:
- it runs in a goroutine;
- execution always continues immediately;
- its boolean return value is ignored;
- it receives a copied
MessageContext, not the original pointer.
That means async middleware is appropriate for:
- fire-and-forget logging;
- metrics;
- telemetry;
- best-effort notifications.
It is not appropriate for:
- access control;
- required validation;
- mutating context values that the handler must read;
- any logic that must deterministically stop execution.
Why async middleware gets a copied context
When middleware is async, the library copies MessageContext before starting the goroutine. This prevents obvious data races against the handler path.
Practical consequence:
- changes you make to the copied
ctxinside async middleware are local to that goroutine; - handlers and later middleware will not see those changes.
So this pattern does not work:
bad := laniakea.NewMiddleware("bad", func(ctx *laniakea.MessageContext, db *App) bool {
ctx.Text = "rewritten"
return false
}).SetAsync(true)
The handler chain will still continue, and the rewritten text will not become the canonical handler context.
Bot-level ordering
Bot-level middleware is the only layer that has explicit sorting support.
Bot.AddMiddleware(...) sorts middleware by:
orderascending;- then by
namelexicographically when orders are equal.
You can set the order with SetOrder(...):
logMW := laniakea.NewMiddleware("log", logFn).SetOrder(10)
authMW := laniakea.NewMiddleware("auth", authFn).SetOrder(20)
bot.AddMiddleware(authMW, logMW)
Even though authMW is added first here, logMW runs first because its order is lower.
Plugin and command ordering
Plugin-level and command-level middleware keep insertion order.
That means:
Plugin.AddMiddleware(a).AddMiddleware(b)runsa, thenb;cmd.Use(a).Use(b)runsa, thenb.
If you need precise phase control across the whole bot, prefer putting those checks into bot-level middleware where ordering is explicit.
Where middleware fits in routing
The routing behavior matters when deciding where to attach middleware:
- bot-level middleware sees every update, even updates that never match a command or plugin;
- plugin-level middleware runs only after the bot has already matched the target plugin;
- command-level middleware runs only after the command or payload was resolved and arguments were parsed for that command structure.
This usually means:
- use bot-level middleware for global gates and observability;
- use plugin-level middleware for module-local policy;
- use command-level middleware for one handler's special preconditions.
Common patterns
Global authorization gate
bot.AddMiddleware(
laniakea.NewMiddleware("private-only", func(ctx *laniakea.MessageContext, db *App) bool {
if ctx.Chat == nil || ctx.Chat.Type != "private" {
return false
}
return true
}),
)
Plugin-wide admin policy
admin := laniakea.NewPlugin[*App]("admin")
admin.AddMiddleware(
laniakea.NewMiddleware("admin-only", func(ctx *laniakea.MessageContext, db *App) bool {
return ctx.From != nil && db.IsAdmin(ctx.From.ID)
}),
)
Command-specific validation
ban := admin.NewCommand(banUser, "ban")
ban.Use(laniakea.NewMiddleware("require-reply", func(ctx *laniakea.MessageContext, db *App) bool {
if ctx.Msg == nil || ctx.Msg.ReplyToMessage == nil {
ctx.Answer("Reply to a user message first")
return false
}
return true
}))
Caveats
- Middleware with an empty name is skipped by
Bot.AddMiddleware(...). - Async middleware cannot block execution.
- Async middleware should avoid depending on mutable shared state unless you provide your own synchronization.
- Plugin middleware and command middleware are snapshotted when the plugin is registered with
Bot.AddPlugins(...), so finish configuring them before registration. - For non-command update handlers, plugin chains are isolated by cloned contexts; for matched commands and payloads, processing stops after the first matching plugin handles the update.
Recommendations
- Default to synchronous middleware unless you specifically want fire-and-forget behavior.
- Keep middleware small and single-purpose.
- Put denial responses close to the gate that makes the decision.
- Prefer bot-level middleware for global concerns and explicit ordering.
- Use async middleware only for side effects that are safe to lose or reorder.
Navigation
Start here
Runtime and Architecture
- Bot-Lifecycle
- Webhook-Runtime
- Middleware
- Runners
- Error-Handling
- Logging
- Update-Routing-Model
- Policies
- Scenes
Interaction and Telegram API
- Inline-Keyboards-and-Payloads
- Auto-Generated-Commands
- Drafts
- Rich-Messages
- Localization
- Rate-Limiting
- tgapi-Overview