Safeguard
Security Guides•

What happens when a Go HTTP handler panics?

Go's net/http server recovers a handler panic, logs a stack trace and drops the connection. Add your own recovery middleware to control the response.

Safeguard Security Team
4 min read

When a Go HTTP handler panics, net/http recovers it, logs a stack trace to the server error log and closes the connection or resets the stream. The process keeps running. The client sees a dropped connection, not a clean error, so add recovery middleware if you want a controlled 500.

What the server does

The Go source states that if ServeHTTP panics, the server assumes the effect of the panic was isolated to the active request. It recovers the panic, logs a stack trace to the server error log, and either closes the network connection or sends an HTTP/2 stream reset. To abort a handler on purpose without a stack trace, panic with the sentinel value http.ErrAbortHandler, which also suppresses the log entry.

The server recovers only panics in the goroutine that runs your handler. If your handler starts its own goroutine and that goroutine panics, nothing recovers it and the whole process exits.

Why this matters for security

Three things go wrong around panics.

  1. Information in logs. A stack trace can include file paths, function arguments and sometimes values. Treat the server error log as sensitive.
  2. Unclean client behaviour. A dropped connection can look like a network fault and trigger client retries, repeating whatever caused the panic.
  3. Crashes from background goroutines. One unrecovered panic in a goroutine you started ends the service, which an attacker may be able to trigger with crafted input.

A recovery middleware

func recoverer(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if v := recover(); v != nil {
                if v == http.ErrAbortHandler {
                    panic(v)
                }
                slog.Error("handler panic", "path", r.URL.Path, "panic", v)
                http.Error(w, "internal error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

Note the generic body. The details go to the log, not the client. Log the request path and a request identifier, and avoid logging the full request body or headers, which may hold tokens.

Testing it

Write a test that registers a handler which panics on a specific input, serve it with httptest, and assert on the status code and body. Check that the body contains no panic text and that the log line carries a request identifier. Then repeat with a panic inside a goroutine you started, wrapped in your own recovery helper, to confirm errors reach your reporting instead of ending the process.

Steps

  1. Add one recovery middleware at the top of the chain so every route is covered.
  2. Re-panic with ErrAbortHandler when you meet it, so intentional aborts keep their meaning.
  3. Wrap every goroutine you start in a function that recovers and reports errors.
  4. Send a generic message to the client and a request identifier you can look up in logs.
  5. Alert on panic counts. A rise often means someone found an input that breaks a handler.
  6. Fuzz the parsing code. Go has native fuzzing built in.

The OWASP error handling guidance points the same way: return a generic response and keep the details in server side logs.

Try it yourself

Click through it at your own pace, or open it full screen. Look at how a code scan locates risky patterns in source, the same way you would find unrecovered goroutines.

Quick check

Does TimeoutHandler recover panics?

It catches a panic from the wrapped handler and raises it again in the calling goroutine, as the Go source shows, so recovery middleware placed outside it still works.

Should I let the process crash and restart instead?

For a request scoped panic, recovering and returning 500 is usually safer than dropping every in flight request. For corrupted shared state, a restart may be right. Decide per service.

Safeguard's dependency scanning and SBOM generation cover the Go modules your service builds from, with reachability analysis to separate reachable findings, and the free scan is at gold.safeguard.sh/scan.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.