skip to content

Which http.Server protections stop applying to a connection once a handler hijacks it?

level: seniorimportance: should knowfreq 34%

answer

  1. the server stopped managing this one
  2. no timeout is re-armed after the handover
  3. the ConnState hook goes quiet
  4. hijacked is a terminal state, closed never comes

basics

~20 s

All of the ones the server enforces per request: its read and write timeouts are no longer renewed, keep-alive limits no longer apply, nothing closes the socket, and the ConnState hook reports StateHijacked as a terminal state, so the connection disappears from the server's accounting.

solid answer

~50 s

After a hijack net/http does nothing further with that connection, so every bound it applied disappears with it. `Server.ReadTimeout`, `Server.WriteTimeout`, `Server.ReadHeaderTimeout` and `Server.IdleTimeout` are enforced by the server around request serving and are no longer renewed or imposed; the connection is never returned to keep-alive; and the server will not close the socket even when the handler returns. The `Server.ConnState` hook reports `http.StateHijacked`, which is documented as terminal — no `StateClosed` follows it — so a gauge that counts new connections and decrements on close drifts upward forever. One sharp edge cuts the other way: a write deadline the server already armed for this request is still on the socket and can kill the upgraded session at that instant, so a hijacking handler has to take charge of the connection's deadlines itself, bound the session's lifetime explicitly, and count and close these connections in its own code.

code

go · 10 lines
go
srv := &http.Server{
	Addr:         ":8080",
	ReadTimeout:  5 * time.Second,
	WriteTimeout: 10 * time.Second,
	IdleTimeout:  60 * time.Second,
	ConnState: func(c net.Conn, s http.ConnState) {
		// StateHijacked is terminal: no StateClosed will follow for this conn.
		log.Printf("%s -> %s", c.RemoteAddr(), s)
	},
}

go deeper

for a junior

Remember the headline: once a connection is hijacked the server stops managing it, so its timeouts and its automatic close no longer apply and your code has to do both.

for a middle

Be able to list the specific settings that stop mattering — the server's read, write and idle timeouts, keep-alive reuse — and to explain why they stop, namely that the server never serves that connection again.

for a senior

Demonstrate the operational reasoning: how you detect leaked sessions with the ConnState hook and the goroutine profile, what replacements you build for bounds and metrics, and the leftover-deadline surprise that kills long sessions at a fixed offset.

for a principal

Own the consequence rather than the mechanism. Connections invisible to the server's accounting undermine capacity planning and incident response, so decide what compensating instrumentation is mandatory before such a route exists.

## The single sentence that explains all of it `net/http` will not do anything else with a hijacked connection. Every protection people think of as "the server's" is implemented by the server *doing something* — arming a deadline before each request, deciding whether to reuse the connection, closing it when a request fails or the peer goes away, reporting state transitions. Stop the server from doing anything and all of it stops together. ## What you lose, concretely **Read and write timeouts.** `Server.ReadTimeout`, `Server.ReadHeaderTimeout` and `Server.WriteTimeout` are per-request bounds that the server applies as it serves. A hijacked connection is never served again, so nothing is re-armed. An upgraded session can now sit open for days. **Idle handling and keep-alive.** `Server.IdleTimeout` reaps connections between requests. Yours is no longer between requests; it will never be considered idle, never be reused, and never be reaped. **Closing.** Returning from the handler closes nothing. The socket lives until your code closes it, the peer disconnects, or the process exits. A handler that spawns a goroutine to service the session and forgets who owns `Close` produces a permanent leak of a file descriptor plus that goroutine. **Connection accounting.** `Server.ConnState` is the hook that reports `StateNew`, `StateActive`, `StateIdle`, `StateHijacked` and `StateClosed`. `StateHijacked` is a terminal state: the connection will never report `StateClosed`. Any metric built as "increment on new, decrement on closed" therefore counts every upgraded session as permanently open, and your dashboard slowly stops describing reality. **Per-request observability.** Middleware that measures status codes and durations sees a handler that returned — usually with no status written, because you wrote the status line by hand. Latency histograms get a meaningless sample, or none. ## The edge that surprises people "The server's timeouts no longer apply" is not quite the same as "there are no deadlines". Those timeouts are implemented as deadlines on the underlying connection, armed before the handler runs. After the hijack the server never touches them again — but an absolute deadline that was already armed does not disappear. With a `WriteTimeout` configured, a long-lived upgraded session can therefore die abruptly at exactly that offset from the request, which reads in production as "every attach session dies after N seconds" and sends people hunting for a proxy or a load balancer. The fix is on your side of the fence: a handler that hijacks must take charge of the connection's deadlines rather than inheriting whatever the server left behind. ## What you must put back For each guarantee you dropped, you own a replacement: - **A session bound.** Decide the maximum lifetime and idle period of an upgraded session and enforce them yourself. - **A close path.** One owner per connection, one `Close`, and a way for the rest of the process to reach it. - **Your own registry and gauge.** Track live sessions in the handler — register on hijack, deregister in the same defer that closes — because `ConnState` will not tell you when they end. - **Your own metrics.** Session count, session duration and bytes moved, recorded by the upgrade code rather than by request middleware. ## Diagnosing it after the fact The cheapest instrument is the `ConnState` hook itself: log every transition with the remote address, and the pattern is unmistakable — a stream of `StateHijacked` lines with no matching `StateClosed`, while the process's file-descriptor count and goroutine count climb in step. Cross-check with the goroutine profile, where every stuck session shows the same read frame in your protocol loop.

  • Why does a connection gauge built on Server.ConnState drift upward when handlers hijack?
    The usual gauge increments on StateNew and decrements on StateClosed. A hijacked connection reports StateHijacked and then nothing at all, so its increment is never undone. Count upgraded sessions in the handler instead — register when you hijack, deregister in the same defer that closes the connection.
  • Does the handler returning close a hijacked connection?
    No. The server does nothing further with that socket, including closing it, so the handler returning is not a lifecycle event for the connection. Exactly one goroutine must own the Close, usually via a defer in whichever function serves the session to its end.
  • With Server.WriteTimeout set, is an upgraded connection really free of deadlines?
    No, and this is the trap. The deadline the server armed for this request is still on the socket, so the session can die at that offset even though nothing will ever re-arm it. A hijacking handler has to take charge of the connection's deadlines itself rather than inherit the leftovers.
  • What does the goroutine profile show when hijacked sessions leak?
    A growing set of goroutines all parked in the same read call inside your protocol loop, one per abandoned session, with matching growth in open file descriptors. Paired with ConnState logs full of StateHijacked and no StateClosed, that is a conclusive diagnosis.

saying these in an interview costs you the question

  • Assumes Server.ReadTimeout still bounds an upgraded session
  • Expects StateClosed to follow StateHijacked in the ConnState hook
  • Thinks returning from the handler releases the socket
  • Relies on request middleware to measure an upgraded session
  • Believes no deadline can fire because the server stopped managing the conn