Skip to main content

Gin Middleware Ordering

When building web applications with Gin, understanding how middleware execution order works is crucial for implementing proper request handling flows. In this guide, we'll explore how Gin executes middleware and how you can control the execution order to achieve the desired behavior in your applications.

Introduction to Middleware Ordering

Middleware in Gin allows you to process HTTP requests before they reach your route handlers or after the response has been generated. The order in which middleware runs can significantly impact your application's behavior, especially when dealing with authentication, logging, error handling, and other cross-cutting concerns.

Gin executes middleware in the order they are added to the engine or router group. This follows a pattern often referred to as "onion layer" architecture, where each middleware wraps around the next one in the chain.

Middleware Onion Model

Basic Middleware Execution Flow

Let's understand the basic flow with a simple example:

go
package main

import (
"fmt"
"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()

// Middleware 1
r.Use(func(c *gin.Context) {
fmt.Println("Middleware 1: Before request")
c.Next()
fmt.Println("Middleware 1: After response")
})

// Middleware 2
r.Use(func(c *gin.Context) {
fmt.Println("Middleware 2: Before request")
c.Next()
fmt.Println("Middleware 2: After response")
})

r.GET("/", func(c *gin.Context) {
fmt.Println("Handler executing")
c.String(200, "Hello, World!")
})

r.Run(":8080")
}

When you run this code and visit http://localhost:8080/, the console output will show:

Middleware 1: Before request
Middleware 2: Before request
Handler executing
Middleware 2: After response
Middleware 1: After response

This demonstrates the "onion layer" execution:

  1. Request flows inward through middleware (from first added to last)
  2. Reaches the handler
  3. Response flows outward through middleware (from last added to first)

Understanding c.Next() and Flow Control

The c.Next() function is key to understanding middleware execution. When called, it executes the next middleware in the chain. If omitted, the execution stops at the current middleware.

Let's see what happens when we omit c.Next() in the second middleware:

go
// Middleware 2 - Without c.Next()
r.Use(func(c *gin.Context) {
fmt.Println("Middleware 2: Before request")
// c.Next() is omitted intentionally
fmt.Println("Middleware 2: After (immediately)")
})

The output would be:

Middleware 1: Before request
Middleware 2: Before request
Middleware 2: After (immediately)
Middleware 1: After response

Notice that the handler never executes because the second middleware doesn't call c.Next().

Middleware Ordering at Different Levels

Gin allows you to define middleware at different levels:

  1. Global middleware - applies to all routes
  2. Group-specific middleware - applies only to routes in that group
  3. Route-specific middleware - applies only to a specific route

The execution order follows the registration order across these levels:

go
package main

import (
"fmt"
"github.com/gin-gonic/gin"
)

func main() {
r := gin.New() // Using New() instead of Default() to avoid built-in middleware

// Global middleware
r.Use(func(c *gin.Context) {
fmt.Println("Global middleware")
c.Next()
})

// Group with middleware
api := r.Group("/api")
api.Use(func(c *gin.Context) {
fmt.Println("API group middleware")
c.Next()
})

// Route with its own middleware
api.GET("/data",
func(c *gin.Context) {
fmt.Println("Route-specific middleware")
c.Next()
},
func(c *gin.Context) {
fmt.Println("Handler executing")
c.JSON(200, gin.H{"data": "success"})
},
)

r.Run(":8080")
}

When accessing /api/data, the execution order will be:

Global middleware
API group middleware
Route-specific middleware
Handler executing

Practical Example: Authentication and Logging

Let's implement a more practical example with authentication and logging middleware:

go
package main

import (
"github.com/gin-gonic/gin"
"log"
"time"
)

func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path

// Process request
c.Next()

// After request
latency := time.Since(start)
statusCode := c.Writer.Status()
log.Printf("[%d] %s %s - %v", statusCode, c.Request.Method, path, latency)
}
}

func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token != "valid-token" {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return
}
c.Next()
}
}

func main() {
r := gin.New()

// Apply logger to all routes - it will log both successful and failed requests
r.Use(Logger())

// Public endpoints
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})

// Protected group
protected := r.Group("/api")
protected.Use(AuthRequired()) // Auth middleware applied after logger

protected.GET("/data", func(c *gin.Context) {
c.JSON(200, gin.H{"data": "secret stuff"})
})

r.Run(":8080")
}

In this example:

  1. The logger middleware runs first for all requests
  2. For /api/* routes, the auth middleware runs after logging but before the handler
  3. If authentication fails, the flow stops at the auth middleware (using c.Abort())
  4. Even for failed auth, the logger will record the completed request (with 401 status)

The Importance of Ordering

The order of middleware can significantly impact your application's behavior. Consider these common scenarios:

Authentication Before Rate Limiting

go
// Good: Auth before rate limiting
r.Use(AuthMiddleware())
r.Use(RateLimitMiddleware())

This approach is good because:

  • Unauthorized requests are rejected early
  • Rate limits only apply to authenticated users
  • Protects against authentication brute force attacks

Recovery Before Other Middleware

go
// Good: Recovery first
r.Use(gin.Recovery())
r.Use(LoggerMiddleware())
r.Use(AuthMiddleware())

This ensures that panics in any middleware or handler will be caught.

Response Compression After Other Middleware

go
// Good: Compression last
r.Use(LoggerMiddleware())
r.Use(AuthMiddleware())
r.Use(CompressionMiddleware())

This allows compression to work on the final response after all modifications.

Working with c.Abort()

Sometimes you need to stop the middleware chain execution. The c.Abort() method prevents pending middleware from running but doesn't stop the current middleware:

go
func conditionalMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if someCondition {
c.AbortWithStatusJSON(403, gin.H{"error": "forbidden"})
// Code here still runs!
return // Use return to exit the middleware function
}

// Only executed if not aborted
c.Next()
}
}

Real-world Application: Role-based Access Control

Here's a complete example implementing role-based access control with properly ordered middleware:

go
package main

import (
"github.com/gin-gonic/gin"
"log"
"time"
)

type User struct {
ID string
Role string
Name string
}

func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path

c.Next()

latency := time.Since(start)
statusCode := c.Writer.Status()
log.Printf("[%d] %s %s - %v", statusCode, c.Request.Method, path, latency)
}
}

func Authentication() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")

// Very simplified auth - in real apps, validate JWT or session
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "authentication required"})
return
}

// Simulate fetching user from database based on token
var user User
if token == "admin-token" {
user = User{ID: "1", Role: "admin", Name: "Admin User"}
} else if token == "user-token" {
user = User{ID: "2", Role: "user", Name: "Regular User"}
} else {
c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
return
}

// Save user in context for other middleware and handlers
c.Set("user", user)
c.Next()
}
}

func AdminOnly() gin.HandlerFunc {
return func(c *gin.Context) {
userAny, exists := c.Get("user")
if !exists {
c.AbortWithStatusJSON(500, gin.H{"error": "user not found in context"})
return
}

user, ok := userAny.(User)
if !ok {
c.AbortWithStatusJSON(500, gin.H{"error": "invalid user type"})
return
}

if user.Role != "admin" {
c.AbortWithStatusJSON(403, gin.H{"error": "admin access required"})
return
}

c.Next()
}
}

func main() {
r := gin.New()
r.Use(gin.Recovery())
r.Use(Logger()) // Applied to all routes

// Public routes
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "public endpoint"})
})

// Authenticated routes
auth := r.Group("/auth")
auth.Use(Authentication())

auth.GET("/profile", func(c *gin.Context) {
user, _ := c.Get("user")
c.JSON(200, gin.H{"user": user})
})

// Admin-only routes
admin := auth.Group("/admin")
admin.Use(AdminOnly())

admin.GET("/stats", func(c *gin.Context) {
c.JSON(200, gin.H{
"stats": "important admin data",
"active_users": 1234,
})
})

r.Run(":8080")
}

To test this application:

  1. Public endpoint: curl http://localhost:8080/
  2. User profile (authenticated): curl -H "Authorization: user-token" http://localhost:8080/auth/profile
  3. Admin endpoint with user token: curl -H "Authorization: user-token" http://localhost:8080/auth/admin/stats (will return 403)
  4. Admin endpoint with admin token: curl -H "Authorization: admin-token" http://localhost:8080/auth/admin/stats (will succeed)

Note the middleware ordering:

  1. Recovery (catches panics)
  2. Logger (logs all requests)
  3. Authentication (secures routes)
  4. AdminOnly (further restricts access)

This ensures errors are caught, everything is logged, and security is properly enforced.

Summary

Understanding middleware order in Gin is essential for building robust web applications. Here are the key points to remember:

  1. Middleware executes in the order it's added to the router or group
  2. The c.Next() function passes control to the next middleware in the chain
  3. Middleware can perform tasks before and after the request is handled
  4. Use c.Abort() to stop the middleware chain execution
  5. Consider the logical dependencies between middleware when determining order
  6. Global middleware is executed before group middleware, which runs before route middleware

By properly ordering your middleware, you can create clean, maintainable, and secure web applications with Gin.

Additional Resources

Exercises

  1. Create a middleware that measures the response time and adds it as a header to the response
  2. Implement a caching middleware that executes before your route handler but after authentication
  3. Build an application with at least three different middleware components and experiment with different ordering to observe the effects
  4. Create a middleware that injects correlation IDs into the logger for request tracing across multiple services

By mastering middleware ordering, you'll be able to build more sophisticated Gin applications with clean, maintainable code structures.



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