Common Go Web Server Errors and What They Really Mean
Reproduce the errors Go HTTP servers and clients print, from address already in use to HTTP response to HTTPS client, and see what each message means.

Go error messages are unusually informative, and they are also easy to misread, because the text is written from the point of view of the library and the person reading it is looking at a symptom: a blank browser page, a failing health check, a log full of stack traces. The error that appears on one side of a connection is often a consequence of something logged on the other. This page collects the ones I see most often in HTTP servers and clients, reproduces each with a few lines of Go, and says what it means and where to look next. The text of every message below is real output from Go 1.25.6.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
It is a companion to the list of server mistakes, which covers the causes, and sits in the web cluster that ends in a production HTTP server. The program that produced the messages is shown once, below, and the sections after it explain them in groups.
All fifteen in one program
Ports vary from run to run, so the program replaces them with PORT. Each line is the actual err.Error(), response body or server log line.
package main
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"sync"
"time"
)
var portRe = regexp.MustCompile(`(127\.0\.0\.1|\[::1\]):\d+`)
func mask(s string) string { return portRe.ReplaceAllString(s, "${1}:PORT") }
type syncBuf struct {
mu sync.Mutex
b bytes.Buffer
}
func (s *syncBuf) Write(p []byte) (int, error) { s.mu.Lock(); defer s.mu.Unlock(); return s.b.Write(p) }
func (s *syncBuf) String() string { s.mu.Lock(); defer s.mu.Unlock(); return s.b.String() }
func show(label, text string) {
text = mask(strings.TrimSpace(text))
if i := strings.IndexByte(text, '\n'); i >= 0 {
text = text[:i]
}
fmt.Printf("%-28s %s\n", label+":", text)
}
func main() {
// 1. Two listeners on one address.
ln, _ := net.Listen("tcp", "127.0.0.1:0")
_, err := net.Listen("tcp", ln.Addr().String())
show("address in use", err.Error())
ln.Close()
// 2. Connection refused.
dead := httptest.NewServer(http.NotFoundHandler())
deadURL := dead.URL
dead.Close()
_, err = http.Get(deadURL)
show("connection refused", err.Error())
plain := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "ok") }))
defer plain.Close()
host := strings.TrimPrefix(plain.URL, "http://")
// 3. HTTPS client against a plain HTTP server.
_, err = http.Get("https://" + host)
show("https to an http server", err.Error())
// 4. Plain HTTP client against a TLS server; and an untrusted certificate.
var tlsLogs syncBuf
tlsSrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
tlsSrv.Config.ErrorLog = log.New(&tlsLogs, "", 0)
tlsSrv.StartTLS()
defer tlsSrv.Close()
res, err := http.Get("http://" + strings.TrimPrefix(tlsSrv.URL, "https://"))
if err == nil {
b, _ := io.ReadAll(res.Body)
res.Body.Close()
show("http to a TLS server", fmt.Sprintf("%d %s", res.StatusCode, b))
}
_, err = http.Get(tlsSrv.URL)
show("untrusted certificate", err.Error())
time.Sleep(50 * time.Millisecond)
for _, line := range strings.Split(strings.TrimSpace(tlsLogs.String()), "\n") {
show(" what the TLS server logged", line[strings.Index(line, "http:"):])
}
// 5. A handler that panics: what the client sees and what the server logs.
var logs syncBuf
panicky := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { panic("boom") }))
panicky.Config.ErrorLog = log.New(&logs, "", 0)
panicky.Start()
defer panicky.Close()
_, err = http.Get(panicky.URL)
show("client after a panic", err.Error())
show("server log after a panic", logs.String())
// 6. A body bigger than MaxBytesReader allows.
big := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 10))
var tooBig *http.MaxBytesError
fmt.Fprintf(w, "%v | MaxBytesError=%v", err, errors.As(err, &tooBig))
}))
defer big.Close()
res, _ = http.Post(big.URL, "text/plain", strings.NewReader(strings.Repeat("x", 100)))
b, _ := io.ReadAll(res.Body)
res.Body.Close()
show("request body too large", string(b))
// 7. Request headers bigger than MaxHeaderBytes.
hdr := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
hdr.Config.MaxHeaderBytes = 512
hdr.Start()
defer hdr.Close()
req, _ := http.NewRequest("GET", hdr.URL, nil)
req.Header.Set("X-Big", strings.Repeat("a", 6000))
res, err = http.DefaultClient.Do(req)
if err == nil {
b, _ := io.ReadAll(res.Body)
res.Body.Close()
show("headers too large", fmt.Sprintf("%d %s", res.StatusCode, b))
}
// 8. Client timeout.
slow := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(300 * time.Millisecond) }))
defer slow.Close()
c := &http.Client{Timeout: 100 * time.Millisecond}
_, err = c.Get(slow.URL)
show("client timeout", err.Error())
// 9. http.TimeoutHandler.
var late error
th := httptest.NewServer(http.TimeoutHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(150 * time.Millisecond)
_, late = w.Write([]byte("too late"))
}), 50*time.Millisecond, "request timed out"))
defer th.Close()
res, _ = http.Get(th.URL)
b, _ = io.ReadAll(res.Body)
res.Body.Close()
time.Sleep(200 * time.Millisecond)
show("TimeoutHandler response", fmt.Sprintf("%d %s", res.StatusCode, b))
show("handler's late Write", fmt.Sprint(late))
// 10. A response that promises more bytes than it sends.
short := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, rw, _ := w.(http.Hijacker).Hijack()
rw.WriteString("HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\nonly ten b")
rw.Flush()
conn.Close()
}))
defer short.Close()
res, _ = http.Get(short.URL)
_, err = io.ReadAll(res.Body)
show("short body", fmt.Sprint(err))
_, err = io.ReadAll(res.Body)
res.Body.Close()
_, err2 := io.ReadAll(res.Body)
show("read after Close", fmt.Sprint(err2))
// 11. A server that does not speak HTTP.
raw, _ := net.Listen("tcp", "127.0.0.1:0")
go func() {
conn, err := raw.Accept()
if err == nil {
conn.Write([]byte("hello, I am not a web server\r\n\r\n"))
conn.Close()
}
}()
_, err = http.Get("http://" + raw.Addr().String())
show("not an HTTP server", err.Error())
raw.Close()
// 12. Shutdown.
srv := &http.Server{Addr: "127.0.0.1:0"}
done := make(chan error, 1)
go func() { done <- srv.ListenAndServe() }()
time.Sleep(50 * time.Millisecond)
srv.Shutdown(context.Background())
show("ListenAndServe after Shutdown", fmt.Sprint(<-done))
}address in use: listen tcp 127.0.0.1:PORT: bind: address already in use
connection refused: Get "http://127.0.0.1:PORT": dial tcp 127.0.0.1:PORT: connect: connection refused
https to an http server: Get "https://127.0.0.1:PORT": http: server gave HTTP response to HTTPS client
http to a TLS server: 400 Client sent an HTTP request to an HTTPS server.
untrusted certificate: Get "https://127.0.0.1:PORT": tls: failed to verify certificate: x509: certificate signed by unknown authority
what the TLS server logged: http: TLS handshake error from 127.0.0.1:PORT: client sent an HTTP request to an HTTPS server
what the TLS server logged: http: TLS handshake error from 127.0.0.1:PORT: remote error: tls: bad certificate
client after a panic: Get "http://127.0.0.1:PORT": EOF
server log after a panic: http: panic serving 127.0.0.1:PORT: boom
request body too large: http: request body too large | MaxBytesError=true
headers too large: 431 431 Request Header Fields Too Large
client timeout: Get "http://127.0.0.1:PORT": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
TimeoutHandler response: 503 request timed out
handler's late Write: http: Handler timeout
short body: unexpected EOF
read after Close: http: read on closed response body
not an HTTP server: Get "http://127.0.0.1:PORT": net/http: HTTP/1.x transport connection broken: malformed HTTP status code "I"
ListenAndServe after Shutdown: http: Server closedWhen the server will not start or cannot be reached
listen tcp 127.0.0.1:PORT: bind: address already in use
Two processes (or two listeners in one program) asked for the same address. It is the first error most developers meet: a previous run of the server is still alive, a debugger is paused on a copy, or a container maps a host port that something else holds. Find the owner with lsof -i :8080 or ss -ltnp, and stop it. It is not a Go problem, but it is a Go message, and the article dedicated to it covers the less obvious causes, including SO_REUSEADDR behavior and TIME_WAIT.
dial tcp 127.0.0.1:PORT: connect: connection refused
Nothing is listening at that address. On the client it means the server is down, listening on a different port, or bound to another interface (localhost resolving to IPv6 ::1 while the server listens on IPv4 only is a classic). In a container it often means the service has not started yet, which a retry with backoff, as in the retry article, absorbs.
net/http: HTTP/1.x transport connection broken: malformed HTTP status code "I"
The client connected and the thing on the other end answered with something that is not HTTP. The "I" here is the first word of the reply the test server wrote. In real life this is a client pointed at a database port, an SSH daemon, or a proxy speaking a different protocol. Check that the host and port are the service you meant.
Scheme mismatches
http: server gave HTTP response to HTTPS client
The client used https:// against a port that speaks plain HTTP. Go noticed that the reply to its TLS handshake was an HTTP response. The fix is on the client URL or on the server: either use http:// or put TLS in front of the port.
400 Client sent an HTTP request to an HTTPS server. and http: TLS handshake error ...
The mirror image. A plain HTTP request reached a TLS listener, which answers with that 400 text, and logs http: TLS handshake error from ...: client sent an HTTP request to an HTTPS server on its side. If you see the log line and no matching request in your access log, that is why: the handshake failed before any request existed.
tls: failed to verify certificate: x509: certificate signed by unknown authority
The client does not trust the issuer of the server's certificate. For a test server or a private CA, give the client the CA (srv.Client() in tests, an x509.CertPool in code, SSL_CERT_FILE in the environment). Never answer this with InsecureSkipVerify. The server logs the other half of it, remote error: tls: bad certificate, which the program printed with it: both lines describe one failed handshake.
Limits, timeouts and what they look like
http: request body too large
Raised by a reader wrapped in http.MaxBytesReader when the body exceeds the limit. Since Go 1.19 the error is a *http.MaxBytesError, so errors.As tells it apart from other read failures, as the program shows with MaxBytesError=true. Map it to 413 Content Too Large (RFC 9110 section 15.5.14). The usual mistake is the opposite: no limit at all. See limiting request body size.
A header larger than MaxHeaderBytes
The server answers 431 Request Header Fields Too Large and closes the connection. The surprise from the run is the margin: in my first attempt, with MaxHeaderBytes set to 512, a 2,000 byte header still got a 200. The server reads up to MaxHeaderBytes plus 4096 bytes before it gives up, so the effective limit is higher than the configured number. The final program therefore sends a 6,000 byte header, which crossed the line. Keep that allowance in mind when you tune the setting for abuse resistance.
context deadline exceeded (Client.Timeout exceeded while awaiting headers)
The client's overall deadline passed before the server sent response headers. The parenthesis tells you the phase; if it says "while reading body" the headers arrived and the body stalled. Fix the slow server, raise the timeout if it is too tight for a legitimate endpoint, or investigate with the connection phases in the keep-alive article. The error text for timeouts is explained further in the client defaults article.
503 request timed out and http: Handler timeout
http.TimeoutHandler answered for a handler that took too long, with the message you gave it. The handler goroutine keeps running; when it finally writes, it gets http: Handler timeout back from Write. That second message is a clue that your handler does not watch its context. TimeoutHandler does not stop the work, it only stops waiting for it, so the handler still needs to honor r.Context().
Errors that come from handlers
EOF on the client and http: panic serving 127.0.0.1:PORT: boom on the server
When a handler panics and nothing recovers, the server logs the panic with a stack trace, closes the connection, and the client, which was waiting for a response, reports EOF. The client message is the unhelpful half; the server log has the cause. A recovery middleware, as in the recover middleware article, turns this into a 500 with a logged stack, which is what you want.
http: superfluous response.WriteHeader call
The handler wrote a status twice, or wrote the body and then set a status. Only the first WriteHeader counts. The usual cause is a missing return after http.Error, covered with a reproduction in the mistakes roundup.
Errors while reading bodies
unexpected EOF
The connection ended before the body did: the response promised 100 bytes and delivered ten. Causes are a server that crashed mid-response, a proxy that closed the connection, or a client that lost the network. Retry idempotent requests; for large downloads, resume with a range request as in the download article. The related unexpected EOF article goes deeper.
http: read on closed response body
Code read res.Body after closing it, usually because of a defer res.Body.Close() in a helper that returns the response, so the caller gets a response whose body is already closed. Read the body inside the function that owns the response, or return the bytes.
Shutdown
http: Server closed
ListenAndServe and Serve return http.ErrServerClosed after Shutdown or Close. It means the server stopped on purpose. Treat it as success: compare with errors.Is(err, http.ErrServerClosed) and do not log it as a failure, or every deploy looks like a crash. See graceful shutdown.
Reading these messages in general
Three habits make most of them easy. Read both sides: for any client error, find the server log line from the same moment, and for any server log, ask what the client saw. Match on types, not strings: errors.Is and errors.As with context.DeadlineExceeded, *http.MaxBytesError, net.Error with Timeout(), http.ErrServerClosed and syscall.ECONNREFUSED are stable, while message text is not. And keep a copy of the exact text when you ask for help, because the difference between connection reset by peer, broken pipe and EOF is the difference between two very different network events, discussed in the articles on connection reset and broken pipe. The standard library documentation lists the sentinel errors in net/http.


