Fixing the 'http: multiple response.WriteHeader calls' Error in Go

beginnerπŸ”· Go2026-04-11| Go (Golang) net/http package, all Operating Systems

Error Message

http: multiple response.WriteHeader calls
#go#golang#http-server#debugging#web-development

The Error

If you've spent much time building web servers in Go, you've likely seen this annoying message pop up in your console:

2023/10/27 10:00:00 http: multiple response.WriteHeader calls

Your server didn't crash. It kept right on running. However, this log is a warning that your request handler is trying to do the impossible: change the past. In Go, once you send an HTTP status code, it’s written in stone for that specific request.

The Root Cause

In the net/http package, http.ResponseWriter is a one-way street. Once the headers are sent, they are gone. This error usually stems from a logic leak where your code attempts to set a status code or write to the response body multiple times.

1. The "Forgotten Return"

This is the culprit 90% of the time. You detect an error, call http.Error(), but then let the function keep running. http.Error calls WriteHeader under the hood. If your code hits another WriteHeader or Write later, Go triggers the warning.

2. Implicit vs. Explicit Headers

Go tries to be helpful. If you call w.Write() without calling w.WriteHeader() first, Go assumes you mean 200 OK and sends that header immediately. If you try to set a different status code after writing data, you're too late.

How to Fix It

Scenario A: The Missing Return

Consider this common mistake. If the id is missing, the code sends a 400 error but doesn't stop. It continues down to the 200 OK logic.

func handler(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    if id == "" {
        http.Error(w, "Missing ID", http.StatusBadRequest)
        // The execution continues here! 
    }

    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Success"))
}

The Fix: Use a guard clause. Always return immediately after sending an error response to stop the function execution right there.

func handler(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    if id == "" {
        http.Error(w, "Missing ID", http.StatusBadRequest)
        return // Correct: Function exits here
    }

    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Success"))
}

Scenario B: Ordering Your Calls

You cannot change the status code once data has hit the wire. This code fails because the 200 OK is sent the moment "Processing..." is written.

func handler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Processing...")) 
    
    // This triggers the error because headers were already sent
    w.WriteHeader(http.StatusCreated) 
}

The Fix: Set your status code and headers before you write a single byte of the body. If you are using json.NewEncoder().Encode(), set your headers first.

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(map[string]string{"status": "done"})
}

Scenario C: Middleware Double-Dipping

Middleware often wraps the next.ServeHTTP(w, r) call. If your middleware writes to the response after calling the next handler, you might clash with what the inner handler already sent.

The Fix: If you need to modify the response after the handler runs, you must use a custom ResponseWriter wrapper to buffer the output or capture the status code before it's sent to the client.

Verification

How do you know it's actually fixed? Don't just guess. Run these checks:

- **Watch the Logs:** Perform the action that triggered the error. If the console stays quiet, you're likely in the clear.
- **Inspect with Curl:** Use `curl -v http://localhost:8080/endpoint`. Look for the `< HTTP/1.1 200 OK` line. Ensure you see exactly one status line and no unexpected body content.
- **Test with NewRecorder:** Use the `httptest` package in your unit tests. It allows you to inspect the `Result().StatusCode` and ensures your logic flows correctly through conditional branches.

Best Practices

- **Return Early:** Adopt the "Return Early" pattern. It keeps your code flat and prevents accidental execution of secondary logic.
- **Single Point of Response:** Try to have only one place in your handler that writes the final success response.
- **Lint Your Code:** Use `golangci-lint`. It can catch some instances where execution paths might lead to multiple writes.
- **Header Order:** Always follow the sequence: 1. Set Headers, 2. WriteHeader, 3. Write Body.

Related Error Notes