What does http.Hijacker's Hijack return, and what does a handler take on by calling it?
answer
- the handler asks for the socket itself
- two things come back, not one
- net/http stops touching this connection
- nothing closes it but your code
basics
~10 sHijack returns the underlying net.Conn plus a *bufio.ReadWriter already buffered on it. From then on net/http writes nothing more on that connection, never reuses it and never closes it. The handler owns the socket.
solid answer
~40 sYou reach it by asserting the ResponseWriter to `http.Hijacker` and calling `Hijack() (net.Conn, *bufio.ReadWriter, error)`; since Go 1.20 `http.NewResponseController(w).Hijack()` is the same thing with an error instead of a failed assertion. On success you get the raw connection and the server's buffered reader/writer for it. net/http then stops touching that connection completely: it writes no status line, applies no keep-alive reuse, and will not close the socket even after the handler returns. The `ResponseWriter` is dead — writes return `http.ErrHijacked` — and `r.Body` must not be used any more. Everything after that point, including the response you owe the client and closing the connection, is your code's job. Only HTTP/1.x connections support it; an HTTP/2 ResponseWriter deliberately does not implement `http.Hijacker`.
code
go · 15 lineshj, ok := w.(http.Hijacker)
if !ok {
// HTTP/2 lands here: its ResponseWriter does not implement Hijacker.
http.Error(w, "upgrade not supported", http.StatusInternalServerError)
return
}
conn, brw, err := hj.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// net/http will neither write to, reuse, nor close this connection again.
// w.Write now returns http.ErrHijacked, and r.Body must not be used.
defer conn.Close()
_ = brwgo deeper
Recall the shape: you assert the ResponseWriter to http.Hijacker, call Hijack, and get the raw connection back. The one fact to hold on to is that the server stops managing that connection entirely.
Be ready to name both return values and say what each is for, and to explain precisely which server behaviours switch off: no response framing, no keep-alive reuse, no close, ErrHijacked on the ResponseWriter.
Show that you know the operational cost before you reach for it. Expect to be asked what you must reimplement once the connection leaves net/http's control, and when a plain streamed response would have done instead.
Own the boundary question: hijacking removes a route from the guarantees the rest of the fleet relies on, so be ready to say when a service is permitted to do it and what it must provide in exchange.
## The interface `net/http` normally owns the TCP connection under a handler: it parses the request, frames whatever you write into a response, decides whether the connection can be reused for the next request, and eventually closes it. Hijacking is the escape hatch that hands that ownership to your handler so you can speak some other protocol over the same socket — an interactive attach or terminal session, a custom line protocol, any upgrade you implement by hand. The surface is one interface: ```go type Hijacker interface { Hijack() (net.Conn, *bufio.ReadWriter, error) } ``` The default HTTP/1.x `ResponseWriter` implements it, so the idiom is a comma-ok assertion: ```go hj, ok := w.(http.Hijacker) if !ok { /* not hijackable */ } conn, brw, err := hj.Hijack() ``` Since Go 1.20 there is also `http.NewResponseController(w).Hijack()`, which returns an error matching `http.ErrNotSupported` rather than making you branch on a failed type assertion. ## What comes back **`net.Conn`** — the actual connection. For a plaintext listener that is a `*net.TCPConn`; behind `ListenAndServeTLS` it is a `*tls.Conn`. Reads and writes on it are raw bytes: no status line, no chunked framing, no header parsing. **`*bufio.ReadWriter`** — the reader and writer net/http was already using on that connection, embedded together as `struct { *bufio.Reader; *bufio.Writer }`. The reader half matters enormously: the server may have read past the end of the request head, so bytes the client has already sent can be sitting in that buffer rather than in the kernel socket. The writer half is buffered, so nothing you write through it reaches the client until you `Flush`. **`error`** — non-nil when this connection cannot be hijacked at all. ## What net/http stops doing After a successful `Hijack`, the server library does nothing further with that connection. Concretely: - It writes no response. If you never write a status line, the client just waits. - `w.Write` and `w.WriteHeader` no longer reach the client; writes return `http.ErrHijacked`. - `r.Body` must not be read after the call — the request body, if any, is now just bytes on the connection you own. - Keep-alive is over: this connection will never carry another request through the mux. - Nothing closes the socket. The handler returning does not close it; only your `Close` does. - The connection reports `http.StateHijacked` to a `Server.ConnState` hook, and that is a terminal state — no `StateClosed` ever follows it. That last group is why the usual shape is `defer conn.Close()` in whichever goroutine owns the connection last. Forget it and you have a socket that lives until the process exits or the peer disappears. ## Where it is unavailable **HTTP/2.** A single HTTP/2 connection multiplexes many streams, so there is no per-request connection to hand over; the HTTP/2 `ResponseWriter` simply does not implement `http.Hijacker` and the assertion fails. A handler that must hijack has to be served over HTTP/1.1 — which is why hand-rolled upgrades and h2 are mutually exclusive on the same route. **Test doubles.** `httptest.ResponseRecorder` has no `Hijack` method either, so a hijacking handler cannot be exercised by passing it a recorder. Test it against a real socket with `httptest.NewServer`. ## When it is the right tool Only when you genuinely need the raw bidirectional byte stream. If the requirement is "push data to the client as it is produced", an ordinary streamed response that the server still frames keeps every guarantee net/http offers you. Hijacking is for the case where the bytes after the handshake are no longer HTTP at all, and it costs you the server's timeouts, its connection accounting and its shutdown handling — all of which you must then reimplement.
- Why does the assertion to http.Hijacker fail for a handler served over HTTP/2?HTTP/2 multiplexes many streams over one TCP connection, so there is no connection belonging to this request that could be handed over. The HTTP/2 ResponseWriter therefore does not implement `http.Hijacker` at all and the comma-ok assertion returns false. A route that hand-rolls an upgrade has to be served over HTTP/1.1.
- What happens if the handler writes to the ResponseWriter after Hijack succeeds?The write returns `http.ErrHijacked` and nothing reaches the client. Once hijacked, the ResponseWriter is inert: every byte the client is going to see must be written through the returned net.Conn or the returned *bufio.ReadWriter.
- Why can't you exercise a hijacking handler with httptest.ResponseRecorder?ResponseRecorder records headers and a body buffer; it has no Hijack method, so the assertion to `http.Hijacker` fails and the handler takes its unsupported branch. Test the real path with `httptest.NewServer`, which gives the handler an actual socket to take over.
saying these in an interview costs you the question
- Thinks the server still closes the connection when the handler returns
- Expects w.WriteHeader to reach the client after hijacking
- Assumes any ResponseWriter can be hijacked, including HTTP/2
- Keeps reading r.Body after the hijack succeeded
- Believes keep-alive still recycles the connection for the next request