---
title: "GN001 – Call c.Abort() After Terminating in Middleware"
impact: high
impactDescription: "Missing c.Abort() causes subsequent middleware handlers and the final route handler to still execute after an early response has been sent, causing duplicate writes and data leaks."
tags: [go, gin, middleware, correctness]
---

# GN001 – Call `c.Abort()` After Terminating in Middleware

## Rule

Any middleware that sends a response and intends to stop the chain **must** call `c.Abort()` (or `c.AbortWithStatus()` / `c.AbortWithStatusJSON()`) immediately after writing. Never rely on `return` alone.

## Why

`c.Next()` executes subsequent handlers in sequence. A plain `return` exits the current handler but Gin still runs the remaining handlers in the chain. `c.Abort()` sets the index past all remaining handlers so the chain stops cleanly.

## Wrong

```go
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
            return   // ❌ returns from this func but downstream handlers still run
        }
        c.Next()
    }
}

func RateLimitMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        if isRateLimited(c.ClientIP()) {
            c.JSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
            // ❌ forgot both return and Abort — next handler fires and writes again
        }
        c.Next()
    }
}
```

## Correct

```go
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
            // c.Abort() is called inside AbortWithStatusJSON — no c.Next() needed
            return
        }
        // validation passes — continue chain
        c.Set("user_id", parsedUserID)
        c.Next()
    }
}

func RateLimitMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        if isRateLimited(c.ClientIP()) {
            c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
            return
        }
        c.Next()
    }
}
```

## Notes

- `c.AbortWithStatus(code)` and `c.AbortWithStatusJSON(code, obj)` both call `c.Abort()` internally — you don't need to call `c.Abort()` separately when using these.
- After `c.Abort()`, code after the call in the same function still runs — use `return` to exit the function body.
- Verify with unit tests: register middleware + handler, send a bad request, assert the handler body was NOT executed.
