How to Use Middleware in Telego Handlers: A Complete Guide

Telego implements middleware as Handler functions that intercept updates via ctx.Next(update), allowing you to compose reusable logic like logging, recovery, and timeouts that execute before your main handlers.

The Telego library (mymmrac/telego) provides a robust handler system for building Telegram bots in Go. Its middleware architecture follows a chain-of-responsibility pattern where each middleware can modify the context, terminate execution, or pass control to the next layer.

Understanding the Middleware Pattern in Telego

In Telego, middleware is not a separate interface but rather the same Handler type used for final update processing. This unified design simplifies the API while enabling powerful composition patterns.

The Handler Signature

Every middleware function conforms to the Handler signature defined in telegohandler/handler_group.go:

func(ctx *telegohandler.Context, update telego.Update) error

The *Context parameter provides access to the bot instance, the current update, and control methods for the execution chain. When your middleware finishes its work, it returns an error to signal whether processing should continue or halt.

The Chain of Responsibility

Middleware executes sequentially based on registration order. The critical mechanism that enables this chaining is the ctx.Next(update) method found in telegohandler/context.go.

When your middleware calls ctx.Next(update), it yields control to the next middleware in the chain or the final handler. If your middleware returns without invoking Next, the chain terminates immediately—subsequent middlewares and handlers never execute. This behavior allows middleware to act as gatekeepers, short-circuiting requests that fail authentication, rate limiting, or validation checks.

Registering Middleware with Use

You attach middleware to the handler system through the Use method available on BotHandler and handler groups. According to telegohandler/bot_handler.go and telegohandler/handler_group.go, this method appends middleware to an ordered slice that the execution engine traverses during update processing.

The Use signature accepts variadic Handler arguments:

func (h *BotHandler) Use(middlewares ...Handler)

Global Middleware Registration

To apply middleware to every incoming update, register it on the base BotHandler instance:

handler, _ := telegohandler.NewBotHandler(bot, updates)

// Apply to all updates
handler.Use(
    telegohandler.PanicRecovery(),
    CustomLoggingMiddleware,
)

Registration order determines execution order. In this example, PanicRecovery executes first, then CustomLoggingMiddleware, and finally the matched handler.

Built-in Middleware Options

Telego provides production-ready middleware implementations in telegohandler/middleware.go that handle common cross-cutting concerns.

PanicRecovery

The PanicRecovery middleware catches panics within the handler chain, logs the stack trace, and prevents the bot from crashing. It returns the panic as an error to the caller, allowing the update processing loop to continue with subsequent updates.

handler.Use(telegohandler.PanicRecovery())

This is essential for production deployments where unhandled panics in one update handler could otherwise terminate the entire bot process.

Timeout

The Timeout middleware enforces a maximum execution duration for the entire handler chain. It wraps the context with a deadline, ensuring that slow handlers or hanging external calls do not block the update processing pipeline indefinitely.

handler.Use(telegohandler.Timeout(5 * time.Second))

When the timeout expires, the context cancellation propagates through the chain, allowing handlers to abort database queries or HTTP requests gracefully.

Creating Custom Middleware

Custom middleware follows the same Handler signature as final handlers but utilizes ctx.Next to continue the chain. This pattern enables you to inject logging, metrics, authentication, or transformation logic.

Logging and Timeout Middleware Example

The following implementation demonstrates a middleware that logs update IDs and applies a 2-second timeout to subsequent processing, adapted from the patterns in telegohandler/context.go:

func LogAndTimeout(next telegohandler.Handler) telegohandler.Handler {
    return func(ctx *telegohandler.Context, upd telego.Update) error {
        // Log incoming update
        ctx.Bot().Logger().Infof("Processing update %d", upd.UpdateID)

        // Apply timeout to the rest of the chain
        ctx, cancel := ctx.WithTimeout(2 * time.Second)
        defer cancel()

        // Continue to next middleware or handler
        return next(ctx, upd)
    }
}

Register this middleware using the Use method to apply it globally, or attach it to specific handler groups for targeted protection.

Conditional Short-Circuiting

Middleware can terminate the chain early by returning without calling next. This is useful for rate limiting or access control:

func RateLimit(next telegohandler.Handler) telegohandler.Handler {
    return func(ctx *telegohandler.Context, upd telego.Update) error {
        if isRateLimited(upd.Message.From.ID) {
            // Stop chain execution; handler never runs
            return nil
        }
        return next(ctx, upd)
    }
}

Scoped Middleware with Handler Groups

Telego supports hierarchical handler groups that allow middleware to apply only to specific subsets of handlers. The Group method, defined in telegohandler/handler_group.go, creates a new HandlerGroup that inherits middleware from its parent but can register its own.

Creating Isolated Groups

// Create a group for admin commands
adminGroup := handler.Group()
adminGroup.Use(AdminAuthMiddleware)

// This handler runs with AdminAuthMiddleware
adminGroup.Handle(func(ctx *telegohandler.Context, upd telego.Update) error {
    // Admin-only logic
    return ctx.Bot().SendMessage(telego.SendMessageParams{
        ChatID: upd.Message.Chat.ID,
        Text:   "Admin command executed",
    })
})

Middleware Execution Order

When processing an update, the execution order follows the group's nesting hierarchy:

  1. Global middleware (registered on base BotHandler)
  2. Parent group middleware
  3. Child group middleware
  4. Final handler

This cascading structure allows you to compose reusable security, logging, and transformation layers that apply only where needed without polluting the global namespace.

Summary

  • Middleware is a Handler: Telego treats middleware as standard Handler functions with the signature func(ctx *telegohandler.Context, update telego.Update) error, unifying the API surface.
  • Chaining via Next: Middleware controls execution flow through ctx.Next(update); omitting this call short-circuits the chain, preventing subsequent handlers from running.
  • Registration with Use: Attach middleware globally or to specific groups using the Use method, which appends handlers to an ordered slice processed sequentially.
  • Built-in Protection: Leverage PanicRecovery and Timeout from telegohandler/middleware.go to harden production bots against crashes and hanging operations.
  • Hierarchical Scoping: Create isolated middleware layers using Group() to apply authentication, logging, or rate limiting only to specific handler subsets without affecting global routing.

Frequently Asked Questions

How does middleware execution order work in Telego?

Middleware executes in the exact order it was registered via the Use method. When an update arrives, the Context.Next method iterates through this ordered slice, invoking each middleware sequentially. If a middleware calls ctx.Next(update), control passes to the next element; if it returns without calling Next, the chain terminates immediately and no subsequent middleware or handlers execute.

What is the difference between a middleware and a handler in Telego?

There is no structural difference—both conform to the Handler signature func(ctx *telegohandler.Context, update telego.Update) error. The distinction is behavioral: middleware is designed to intercept updates and delegate to the next layer via ctx.Next(update), while final handlers typically process the update directly without calling Next. This unified design allows the same function to act as middleware in one context and a final handler in another.

Can I apply middleware to only specific commands or message types?

Yes, through the handler group hierarchy. Instead of registering middleware globally on the BotHandler instance, create a subgroup using the Group() method and attach middleware to that specific group using Use. Only handlers registered within that group or its descendants will execute that middleware. This pattern is ideal for isolating authentication, administrative checks, or specialized logging to specific command subsets.

How do I prevent a handler from running if middleware validation fails?

Return from the middleware without invoking ctx.Next(update). When middleware returns nil or an error without calling Next, the Context.Next execution engine stops traversing the chain immediately. Subsequent middlewares and the final handler never receive the update. This short-circuit pattern is commonly used for rate limiting, IP blocking, or permission denial, where the middleware consumes the update and optionally sends an error message to the user.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →