Skip to main content

Echo Middleware Order

When building web applications using the Echo framework in Go, understanding the execution order of middleware is crucial for proper application behavior. In this guide, we'll explore how middleware ordering works in Echo and how it affects your application.

Introduction to Middleware Order

In Echo, middleware functions are executed in a specific order, creating a "pipeline" through which HTTP requests and responses flow. The order in which you register middleware determines how they are executed, which can significantly impact your application's behavior.

Middleware in Echo follows a "Russian doll" model - middleware registered first will be executed first during the request phase and last during the response phase.

How Middleware Execution Works in Echo

To understand middleware order, let's first review the basic structure of Echo middleware:

go
func MyMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Code executed during request phase (before next)

err := next(c) // Call the next middleware or handler

// Code executed during response phase (after next)
return err
}
}

When a request comes in, Echo processes it through registered middleware in the order they were added. The execution follows this pattern:

  1. Request comes in
  2. Middleware 1 request code runs
  3. Middleware 2 request code runs
  4. ...
  5. Handler executes
  6. Middleware N response code runs
  7. ...
  8. Middleware 2 response code runs
  9. Middleware 1 response code runs
  10. Response goes out

Basic Example of Middleware Order

Let's see this in action with a simple example:

go
package main

import (
"fmt"
"github.com/labstack/echo/v4"
"net/http"
)

func main() {
e := echo.New()

// First middleware
e.Use(firstMiddleware)

// Second middleware
e.Use(secondMiddleware)

// Route
e.GET("/", func(c echo.Context) error {
fmt.Println("Handler executing")
return c.String(http.StatusOK, "Hello, World!")
})

e.Start(":8080")
}

func firstMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
fmt.Println("First middleware - before")
err := next(c)
fmt.Println("First middleware - after")
return err
}
}

func secondMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
fmt.Println("Second middleware - before")
err := next(c)
fmt.Println("Second middleware - after")
return err
}
}

When you make a request to this server, the console output will be:

First middleware - before
Second middleware - before
Handler executing
Second middleware - after
First middleware - after

This demonstrates the "wrap around" nature of middleware execution.

Practical Applications of Middleware Order

Understanding middleware order is particularly important for certain common use cases:

Authentication and Authorization

Authentication should typically come before authorization:

go
// First check if the user is authenticated
e.Use(authenticationMiddleware)

// Then check if they have permission
e.Use(authorizationMiddleware)

Logging and Error Handling

Logging middleware is often added first to capture all requests, while error handling middleware might be added last to catch any errors from other middleware:

go
// Log all incoming requests
e.Use(loggingMiddleware)

// Other middleware...
e.Use(otherMiddleware)

// Catch any errors that weren't handled
e.Use(errorHandlerMiddleware)

Request Modification

If one middleware modifies the request data that another middleware depends on, the modifying middleware should be registered first:

go
// Parse form data
e.Use(parseFormMiddleware)

// Use the parsed data
e.Use(processFormDataMiddleware)

Group-Level Middleware

Echo also allows you to apply middleware to specific route groups, which creates a more complex execution order:

go
package main

import (
"github.com/labstack/echo/v4"
"net/http"
)

func main() {
e := echo.New()

// Global middleware
e.Use(globalMiddleware)

// Public routes
e.GET("/public", publicHandler)

// Admin group with additional middleware
admin := e.Group("/admin")
admin.Use(adminAuthMiddleware)
admin.GET("/dashboard", adminDashboardHandler)

e.Start(":8080")
}

func globalMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// This runs for all routes
return next(c)
}
}

func adminAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// This runs only for routes in the admin group
return next(c)
}
}

func publicHandler(c echo.Context) error {
// Only goes through globalMiddleware
return c.String(http.StatusOK, "Public page")
}

func adminDashboardHandler(c echo.Context) error {
// Goes through globalMiddleware AND adminAuthMiddleware
return c.String(http.StatusOK, "Admin Dashboard")
}

For the /admin/dashboard route, the middleware execution would be:

  1. globalMiddleware (request phase)
  2. adminAuthMiddleware (request phase)
  3. adminDashboardHandler
  4. adminAuthMiddleware (response phase)
  5. globalMiddleware (response phase)

Middleware Short-Circuiting

Sometimes you want middleware to stop the execution flow and not call subsequent middleware or the handler. For example, authentication middleware might reject unauthorized requests:

go
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")

if token != "valid-token" {
// Short-circuit the middleware chain
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Unauthorized access",
})
}

// Continue to next middleware/handler
return next(c)
}
}

When short-circuiting occurs, the response phase of previously executed middleware will still run, but in reverse order:

Middleware1 (request) -> Middleware2 (request) -> [Middleware2 short-circuits] -> Middleware1 (response)

Best Practices for Middleware Order

Based on common patterns, here's a recommended order for middleware:

  1. Recovery middleware - Catch panics to prevent server crashes
  2. Logging middleware - Log all requests
  3. Security middleware - CORS, XSS protection, etc.
  4. Request ID or tracing middleware - Add request identifiers for tracking
  5. Body limits and parsing middleware - Control request size and parse body
  6. Session/Cookie middleware - Process cookies and session data
  7. Authentication middleware - Verify user identity
  8. Authorization middleware - Check user permissions
  9. Business logic middleware - Application-specific processing
  10. Response compression middleware - Compress response data

Example of applying middleware in a recommended order:

go
e := echo.New()

// Server recovery
e.Use(middleware.Recover())

// Request logging
e.Use(middleware.Logger())

// Security headers
e.Use(middleware.Secure())

// Request ID
e.Use(middleware.RequestID())

// Body limits
e.Use(middleware.BodyLimit("2M"))

// CORS
e.Use(middleware.CORS())

// Authentication
e.Use(customAuthMiddleware)

// Authorization
e.Use(customRoleMiddleware)

// Custom business logic
e.Use(businessLogicMiddleware)

// Routes
e.GET("/", homeHandler)

Common Pitfalls with Middleware Order

1. Authentication After Data Modification

Placing authentication middleware after middleware that modifies request data can lead to security vulnerabilities:

go
// INCORRECT ORDER
e.Use(dataProcessingMiddleware) // Security risk: processes unauthenticated data
e.Use(authenticationMiddleware) // Too late, unauthenticated data already processed

// CORRECT ORDER
e.Use(authenticationMiddleware) // Authenticate first
e.Use(dataProcessingMiddleware) // Then process data from authenticated users

2. Response Modification After Response Has Been Sent

Middleware that tries to modify responses needs to be placed appropriately:

go
func responseHeaderMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
err := next(c)
// This won't work if earlier middleware has already written and sent the response
c.Response().Header().Set("X-Custom-Header", "Value")
return err
}
}

3. Not Considering Response Phase

Remember that middleware also runs during the response phase in reverse order:

go
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Measuring request time
startTime := time.Now()

err := next(c)

// Calculating duration in response phase
duration := time.Since(startTime)
fmt.Printf("Request took %v\n", duration)

return err
}
})

If this timing middleware is added last, it will run first during the response phase, which might not capture the full request duration including other middleware.

Summary

Understanding middleware order in Echo is crucial for building secure and well-functioning web applications:

  • Middleware executes in the order it's registered during the request phase
  • During the response phase, middleware executes in reverse order
  • Group-level middleware creates nested execution chains
  • Middleware can short-circuit the execution chain
  • The order of middleware is particularly important for authentication, authorization, logging, and error handling

By carefully planning your middleware order, you can ensure that your Echo application processes requests and responses correctly, securely, and efficiently.

Additional Resources

Exercises

  1. Create a middleware that logs the time taken for a request to be processed, and place it in different positions in the middleware chain to observe how its measurements change.

  2. Implement a middleware that checks for a special header value and short-circuits the request with a custom response if present.

  3. Build a middleware chain with at least three custom middleware functions that add information to the context during the request phase and log information during the response phase.

  4. Set up different middleware combinations for different route groups and observe how the execution order changes depending on the route being accessed.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)