Fix

"http: wrote more than the declared Content-Length"

Web
Fix the 'http: wrote more than the declared Content-Length' error by ensuring your response body size matches the Content-Length header.

When the manifest does not match the cargo

You are building a REST endpoint that returns a JSON payload. You want to be precise, so you calculate the byte size, set the Content-Length header, and stream the response. The first request works. The second request, with slightly different data, crashes the handler. Your server logs http: wrote more than the declared Content-Length and drops the connection. The client sees a truncated response or a network error. You did not break the internet, but you did break the HTTP contract.

The HTTP framing promise

HTTP treats Content-Length as a strict promise. When a server declares a length, the client allocates exactly that much buffer space. The server is expected to deliver exactly that many bytes, then close or finish the stream. Go's net/http package enforces this promise at the transport layer. If your handler writes past the declared boundary, the standard library refuses to continue. It logs the error, tears down the connection, and prevents malformed responses from reaching the client.

Think of it like a prepaid parking ticket. You tell the booth operator you will stay for two hours. The gate opens. If you stay for three, the system flags an overstay and locks you out. The header is the ticket. The bytes you write are the time you actually spend. Mismatch them, and the protocol rejects you.

HTTP/1.1 and HTTP/2 rely on precise framing. If a server lies about payload size, parsers downstream will read into the next HTTP request or corrupt the multiplexed stream. Go chooses to fail fast rather than send garbage. The standard library tracks every byte that passes through http.ResponseWriter. When the counter exceeds the declared header, it triggers a hard stop.

Headers are promises. Keep them accurate or let Go handle them.

The minimal failure case

package main

import (
	"fmt"
	"net/http"
)

// HandlerWithBug demonstrates the exact condition that triggers the error.
func HandlerWithBug(w http.ResponseWriter, r *http.Request) {
	// The header declares exactly 10 bytes.
	w.Header().Set("Content-Length", "10")
	// The actual payload is 12 bytes. The transport will reject this.
	w.Write([]byte("Hello, World!"))
}

func main() {
	http.HandleFunc("/bug", HandlerWithBug)
	fmt.Println("Server starting on :8080")
	http.ListenAndServe(":8080", nil)
}

Run that server and hit /bug. The ResponseWriter intercepts the w.Write call. It checks the Content-Length header. It starts a byte counter. After writing ten bytes, it hits the limit. The next two bytes trigger an internal check in net/http. The standard library logs http: wrote more than the declared Content-Length to stderr, closes the underlying TCP connection, and returns an error from the write operation. The client never receives the full string. It gets a partial payload and a broken pipe.

The standard library does this intentionally. HTTP/1.1 and HTTP/2 rely on precise framing. If a server lies about payload size, parsers downstream will read into the next HTTP request or corrupt the stream. Go chooses to fail fast rather than send garbage.

Measure before you commit. The transport will not forgive a broken promise.

How the standard library tracks bytes

Under the hood, http.ResponseWriter is an interface. The concrete implementation depends on whether the client negotiated HTTP/1.1 or HTTP/2. Both implementations maintain a byte counter when Content-Length is present. The counter lives in the response writer's internal state, not in your handler. Every call to w.Write, w.WriteHeader, or io.Copy feeds into that counter.

When you call w.Header().Set("Content-Length", "100"), the transport switches to fixed-length mode. It disables chunked encoding. It expects exactly one hundred bytes. If you write ninety, the connection stays open until the client times out. If you write one hundred and one, the transport panics or logs the error and closes the socket. The behavior is identical across HTTP/1.1 and HTTP/2 because the framing contract is protocol-agnostic.

Go developers rarely set Content-Length manually. The net/http package calculates it automatically when you use w.Write on a known []byte or string, and it switches to chunked encoding when the size is unknown. The community convention is to let the standard library manage framing. You only need to set the header explicitly when you are bypassing w.Write with low-level net.Conn operations or when an upstream proxy requires strict length declarations. Trust the transport to handle the plumbing.

Realistic patterns that avoid the trap

In production, you rarely know the exact byte count before serialization. JSON marshaling, template rendering, or database queries produce variable-length output. The fix is to measure before you commit, or skip the header entirely.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

// HandlerWithExactLength calculates the payload size before writing headers.
func HandlerWithExactLength(w http.ResponseWriter, r *http.Request) {
	data := map[string]string{"status": "ok", "message": "payload delivered"}

	// Marshal to bytes first. This gives us the exact size.
	body, err := json.Marshal(data)
	if err != nil {
		// Return a standard error response. Headers are set automatically.
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	// Set the header after measuring. The value must be a string.
	w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
	w.Header().Set("Content-Type", "application/json")

	// Write the pre-measured bytes. The transport will match the header exactly.
	w.Write(body)
}

func main() {
	http.HandleFunc("/exact", HandlerWithExactLength)
	http.ListenAndServe(":8080", nil)
}

This pattern works because json.Marshal returns a []byte. You measure len(body), format it as a string, and set the header. The subsequent w.Write delivers exactly that many bytes. The transport is happy.

Sometimes you cannot measure ahead of time. Streaming responses, server-sent events, or large file transfers require chunked output. In those cases, do not set Content-Length. Let the net/http package fall back to chunked transfer encoding. If you must flush data immediately, call w.Flush() after each chunk. The standard library handles the framing automatically.

// HandlerWithStreaming demonstrates chunked output without a length header.
func HandlerWithStreaming(w http.ResponseWriter, r *http.Request) {
	// Omit Content-Length. The transport will use chunked encoding.
	w.Header().Set("Content-Type", "text/event-stream")

	// Write the first chunk. The client receives it immediately.
	w.Write([]byte("data: chunk one\n\n"))
	w.Flush()

	// Write the second chunk. The transport tracks boundaries automatically.
	w.Write([]byte("data: chunk two\n\n"))
	w.Flush()
}

This approach removes the risk of mismatched lengths entirely. The client reads until the connection closes or the server sends a termination signal. You trade precise length declarations for flexibility. The trade-off is standard in streaming architectures.

Let the protocol choose the framing. You focus on the data.

Pitfalls and debugging the mismatch

Several patterns trigger this error in subtle ways. Setting the header after the first write is a common mistake. HTTP headers must be sent before the body. Once w.Write or w.WriteHeader executes, the header map locks. Calling w.Header().Set("Content-Length", "100") after writing five bytes does nothing. The transport already started streaming with a default or missing length. If you later write ninety more bytes, the client might hang or the server might panic depending on the HTTP version.

Another trap is mixing fmt.Fprintf with manual length calculations. fmt.Fprintf writes directly to the ResponseWriter. If you calculated the length of a string but forgot that fmt.Fprintf adds a newline or encodes differently, the byte count drifts. The compiler will not catch this. You will only see it when the runtime logs http: wrote more than the declared Content-Length or when the client reports a truncated response.

If you accidentally pass a non-string value to the header setter, the compiler rejects the program with cannot use 100 (untyped int constant) as string value in argument to w.Header().Set. Always wrap numeric lengths in fmt.Sprintf("%d", size).

A third pitfall involves middleware. If a logging or compression middleware wraps the ResponseWriter, it might intercept writes and alter the byte count. Some third-party wrappers do not forward Content-Length correctly. When you see this error in a complex stack, check whether an upstream handler modified the response writer. The standard library only tracks what it sees directly.

Debugging this error requires tracing the byte flow. Print the declared length and the actual payload size in your handler. Compare them before writing. If they differ, log the discrepancy and drop the header. Use httputil.DumpResponse in tests to verify that the outgoing headers match the body. Add a middleware that counts bytes and asserts the header at the end of the request lifecycle. Catch the mismatch in development before it reaches production.

The worst goroutine bug is the one that never logs. The same applies to silent header drift.

When to set the header and when to skip it

Use a pre-calculated Content-Length when you serialize the entire payload in memory before sending it. Use chunked transfer encoding when streaming data of unknown size or processing large files. Use http.Flush() when you need to push partial data to the client immediately, such as in server-sent events or progress indicators. Use http.Error for failure paths, since it handles headers and status codes automatically. Use a bytes.Buffer or strings.Builder when you need to assemble a response from multiple sources before measuring the final size. Use explicit header management only when an upstream proxy or load balancer requires strict length declarations for caching or rate limiting.

Where to go next