skip to content

Hijacking and Upgrades

http.Hijacker hands you the raw net.Conn and its buffered reader, after which net/http manages nothing: no timeouts, no Shutdown wait, and you write the 101 response yourself. The upgrade path in Go.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

What does http.Hijacker's Hijack return, and what does a handler take on by calling it?

level: middleimportance: must knowfreq 50%

answer

  1. the handler asks for the socket itself
  2. two things come back, not one
  3. net/http stops touching this connection
  4. nothing closes it but your code

basics

~10 s

Hijack 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 s

You 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 lines
go
hj, 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()
_ = brw

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

How do you write the 101 Switching Protocols response yourself after calling Hijack?

level: middleimportance: should knowfreq 38%

basics

~20 s

Write the raw HTTP/1.1 status line and headers through the *bufio.ReadWriter Hijack returned, end the head with a blank CRLF line, then call Flush. The ResponseWriter cannot do it for you: after a hijack it returns http.ErrHijacked.

open as a page

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

level: seniorimportance: should knowfreq 34%

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.

open as a page

When should a service allow handler hijacking, given it opts those connections out of platform guarantees?

level: principalimportance: should knowfreq 22%

basics

~20 s

Only when the workload genuinely needs a raw bidirectional stream and the team will fund the replacements: an explicit session bound, its own registry and close path, its own metrics, and isolation from routes whose deploy and drain behaviour must stay predictable.

open as a page

Why does reading from the net.Conn returned by Hijack lose bytes the client already sent?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

net/http reads requests through a buffered reader and can read past the end of the request head. Those bytes are already off the socket, so a read on the net.Conn skips them. Read through the *bufio.ReadWriter Hijack returned instead.

open as a page