Gin "Response Writer Already Written" Error When Calling Next()
"Superfluous response.WriteHeader call" or similar already-written errors in Gin almost always mean a response was written more than once for the same request — commonly because a middleware wrote a response and then still called c.Next(), allowing a later handler to also try writing its own response to the same, already-completed request.
The Problem
A request produces unexpected behavior along with a warning or error in the server logs:
http: superfluous response.WriteHeader call from main.someMiddleware (middleware.go:15)
The response the client actually receives might be garbled, incomplete, or simply the first of two responses that were both attempted, depending on exactly how the double-write occurred.
Why It Happens
Gin's middleware chain expects each request to be responded to exactly once, and calling c.Next() after a middleware has already written a response (via c.JSON(), c.String(), or similar) allows execution to continue into the next middleware or handler, which may then also attempt to write its own response — resulting in the "already written" error. Common patterns causing this:
- A middleware that returns an error response for some condition, but forgets to
returnimmediately after writing that response, so execution falls through to a subsequentc.Next()call further down in the same function - A middleware handling an error case that both writes an error response AND calls
c.Next(), under the mistaken assumption thatNext()is always required regardless of whether a response was already sent - Recovery middleware or error-handling logic that writes a response for a recovered panic, but a subsequent piece of middleware in the chain (registered after the recovery middleware, but conceptually "underneath" it in execution) also attempts its own error handling and response for the same failure
The Fix
Always return immediately after writing a response in a middleware when you want to stop the chain there — don't rely on the mere absence of a subsequent c.Next() call alone if the code structure allows control flow to reach further code, including a c.Next() call, after the write:
func authMiddleware(c *gin.Context) {
if !isAuthenticated(c) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
c.Abort()
return // critical - stops this function AND prevents Next() from running
}
c.Next()
}
Use c.Abort() explicitly alongside the early return, not as a substitute for it — Abort() marks the context so that Gin's own internal chain-walking logic knows not to proceed to subsequent middleware/handlers even if something else later in the chain calls c.Next() again, while the return statement stops execution of the current function immediately. Using both together is the robust, idiomatic pattern:
func rateLimitMiddleware(c *gin.Context) {
if isRateLimited(c) {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"})
c.Abort() // prevents subsequent Next() calls in the chain from proceeding
return // stops this specific function immediately
}
c.Next()
}
If you're writing custom error-handling or recovery logic, ensure only one layer in your middleware chain is actually responsible for writing the final error response for a given failure condition — having multiple, independent pieces of middleware each attempting to handle and respond to what's conceptually the same error condition is a common source of this exact conflict, even when each individual piece of middleware looks correct in isolation.
Review middleware registration order carefully, since Gin executes registered middleware in order for the "before Next()" portion of each, but in reverse order for the "after Next()" portion — a middleware registered early that writes a response in its "after" section (code placed after its own c.Next() call) can conflict with a later-registered middleware's own attempt to write a response in its "after" section, if both assume they're the one responsible for the final response:
func loggingMiddleware(c *gin.Context) {
c.Next() // handler and subsequent middleware run first
// this code runs AFTER the handler - avoid writing a response here
// if the handler (or another middleware) might have already written one
log.Printf("Request completed with status %d", c.Writer.Status())
}
Note the use of c.Writer.Status() here rather than attempting to write anything new — logging or reading the already-set status after the fact is safe, but attempting to call c.JSON() or similar again at this point, after the handler already responded, is exactly the pattern causing the original error.
Still Not Working?
If the double-write is happening but the specific middleware responsible isn't obvious from the stack trace alone, add explicit logging at the start of every middleware and handler in your chain, printing whether c.Writer.Written() already returns true at that point — this Gin-provided method tells you definitively whether a response has already been written for the current request, letting you pinpoint exactly which middleware in a longer chain is the first to write, and which subsequent one is incorrectly attempting to write again:
func debugMiddleware(name string) gin.HandlerFunc {
return func(c *gin.Context) {
log.Printf("%s: already written = %v", name, c.Writer.Written())
c.Next()
}
}