When working with Go’s HTTP server, you might encounter the cryptic error message “http: superfluous response.WriteHeader”. This error occurs when your code attempts to modify response headers after the response has already been written. Let’s dive into what causes this and how to fix it.
The Problem
Consider this seemingly straightforward JWT validation middleware:
func ValidateJwt(w http.ResponseWriter, r *http.Request, adminToken string) bool {
if ensureValidJwt(w, r, adminToken) {
return true
}
// Error occurs here
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, err := io.WriteString(w, `{"status": "unauthorized"}`)
if err != nil {
log.Log().Error("routeDeleteSecret",
"msg", "Problem writing response",
"err", errorString)
}
return false
}
At first glance, this code looks correct. We’re:
- Validating the JWT
- Setting the content type header
- Setting the status code
- Writing the response body
However, if you’re getting the “superfluous response.WriteHeader” error, it means something is already writing to the response before these lines execute.
Understanding the HTTP Response Lifecycle
In Go’s HTTP server, there’s a specific order of operations when sending responses:
- Set headers (
w.Header().Set()
) - Set status code (
w.WriteHeader()
) - Write body (
w.Write()
orio.WriteString()
)
Once you perform step 2 or 3, you can’t go back and modify headers or status codes. It’s a one-way street.
Common Causes
The most frequent cause of this error is middleware or helper functions that write to the response writer before returning. In our example, the likely culprit is ensureValidJwt()
. It’s probably writing its own response before returning false
.
Solutions
Option 1: Single Point of Response
Modify your validation function to only handle validation, not responses:
func validateJwtToken(r *http.Request, adminToken string) bool {
// Only validate the token, don't write responses
// Return true if valid, false if invalid
}
func ValidateJwt(w http.ResponseWriter, r *http.Request, adminToken string) bool {
if validateJwtToken(r, adminToken) {
return true
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, err := io.WriteString(w, `{"status": "unauthorized"}`)
if err != nil {
log.Log().Error("routeDeleteSecret",
"msg", "Problem writing response",
"err", errorString)
}
return false
}
Option 2: Consistent Response Handling
If you need the helper function to handle responses, make it responsible for all response writing:
func ensureValidJwt(w http.ResponseWriter, r *http.Request, adminToken string) bool {
// If invalid, handle the entire response here
if !isValid {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, err := io.WriteString(w, `{"status": "unauthorized"}`)
return false
}
return true
}
func ValidateJwt(w http.ResponseWriter, r *http.Request, adminToken string) bool {
// Let ensureValidJwt handle all response writing
return ensureValidJwt(w, r, adminToken)
}
Best Practices
- Single Responsibility: Decide whether a function validates or writes responses, not both
- Clear Documentation: Document whether your functions write to the response writer
- Consistent Patterns: Stick to one pattern throughout your codebase
- Early Returns: Handle error cases early and return to avoid nested conditions
Conclusion
The “superfluous response.WriteHeader” error is Go’s way of telling you that you’re trying to modify a response that’s already been sent. By understanding the HTTP response lifecycle and maintaining clear responsibilities in your functions, you can avoid this error and write more maintainable code.
Remember: Headers → Status → Body. Once you start writing the response, there’s no going back to modify headers or status codes.