Go HTTP Handler Panic Closes the Connection
Fix Go net/http panics that abort a request by recovering in middleware, logging the stack, and returning a controlled 500 response.
Symptoms
- The client sees an empty reply from server instead of a 500
- Server logs show a panic with stack but the client got no body
- Intermittent ECONNRESET on one endpoint
Possible causes
- A handler panics and net/http recovers internally but still aborts the response
- A nil map or slice index writes in a handler on an edge path
- A downstream call returns nil and a method is dereferenced
Diagnosis steps
Capture the panic stack in logs
curl -i localhost:8080/endpoint; grep -n "panic:" server.logInterpretation
The stack frame points at the exact handler line that panicked, which is the true defect.
Add a recovering middleware around the mux
mux := http.NewServeMux(); handler := recoverMiddleware(mux); http.ListenAndServe(":8080", handler)Interpretation
Recovery turns a dropped connection into a controlled 500 the client can handle.
Root cause
net/http recovers a panicking handler to protect the server, but once the handler panics mid-response the reply is aborted; the client sees a closed connection rather than a response, and no error body is produced.
Fix
Wrap the mux with a middleware that recovers, logs the stack with request context, and writes a 500 JSON error if the header has not been sent yet.
Risk note
Recovery must not swallow panics during testing; keep a test mode that re-panics so bugs are visible.
Verify the fix
Trigger the previously panicking endpoint and confirm a 500 with a JSON body plus a logged stack, and that the process stays up.
Symptoms
When you hit “Go HTTP Handler Panic Closes the Connection”, guessing wastes time. The guide below follows evidence: reproduce the symptom, run targeted diagnostics, identify the root cause, apply the fix, and verify it with concrete checks.
Observed symptoms include:
- The client sees an empty reply from server instead of a 500
- Server logs show a panic with stack but the client got no body
- Intermittent ECONNRESET on one endpoint
Reproduce and Diagnose
Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.
Step 1: Capture the panic stack in logs
curl -i localhost:8080/endpoint; grep -n "panic:" server.log
The stack frame points at the exact handler line that panicked, which is the true defect.
Step 2: Add a recovering middleware around the mux
mux := http.NewServeMux(); handler := recoverMiddleware(mux); http.ListenAndServe(":8080", handler)
Recovery turns a dropped connection into a controlled 500 the client can handle.
Likely contributors, in order of frequency:
- A handler panics and net/http recovers internally but still aborts the response
- A nil map or slice index writes in a handler on an edge path
- A downstream call returns nil and a method is dereferenced
Root Cause
net/http recovers a panicking handler to protect the server, but once the handler panics mid-response the reply is aborted; the client sees a closed connection rather than a response, and no error body is produced.
The Fix
Wrap the mux with a middleware that recovers, logs the stack with request context, and writes a 500 JSON error if the header has not been sent yet.
Watch out: Recovery must not swallow panics during testing; keep a test mode that re-panics so bugs are visible.
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("panic on %s %s: %v\n%s", r.Method, r.URL.Path, rec, debug.Stack())
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
Verify and Prevent
Trigger the previously panicking endpoint and confirm a 500 with a JSON body plus a logged stack, and that the process stays up.
Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.
This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.