skip to content

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

level: middleimportance: should knowfreq 38%

answer

  1. no framing library left to help you
  2. you are emitting HTTP/1.1 by hand
  3. CRLF lines, then an empty line
  4. the returned writer is buffered

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.

solid answer

~40 s

Once you hijack, net/http writes nothing on that connection, so the handshake response is bytes you produce by hand: `HTTP/1.1 101 Switching Protocols\r\n`, whatever `Upgrade` and `Connection` headers the peer expects, and then an empty `\r\n` line to terminate the head. Write them through the returned `*bufio.ReadWriter` and then call `Flush` — the writer half is buffered, so without the flush the client sits waiting for a handshake that is still in your process. CRLF line endings are not optional; you are emitting HTTP/1.1 wire format with no library help. After the flush the connection carries the new protocol's bytes only, and the ResponseWriter stays unusable — `w.WriteHeader(http.StatusSwitchingProtocols)` at that point just returns `http.ErrHijacked`.

code

go · 15 lines
go
conn, brw, err := hj.Hijack()
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}
defer conn.Close()

brw.WriteString("HTTP/1.1 101 Switching Protocols\r\n")
brw.WriteString("Upgrade: term\r\n")
brw.WriteString("Connection: Upgrade\r\n")
brw.WriteString("\r\n") // the blank line ends the response head
if err := brw.Flush(); err != nil {
	// nothing reached the client before this call
	return
}

go deeper

for a junior

Know that after a hijack the response is no longer generated for you: the status line is text your code writes to the connection, and buffered writers need flushing before anything leaves the process.

for a middle

Be able to recite the exact head — status line, headers, terminating blank line, all CRLF — and to explain why the Flush call is what actually makes the client see it.

for a senior

Talk about the failure paths: validate and reject before hijacking so ordinary error handling and middleware still work, check the flush error, and know that after the blank line nothing on that socket is HTTP any more.

for a principal

Frame it as a maintenance cost. Hand-rolled wire format in a handler is code nobody else on the team reviews confidently, so decide deliberately whether one route is worth owning a protocol implementation.

## Why you write it at all When a handler hijacks, net/http hands over the socket and stops producing output on it. That includes the response to the very request that triggered the upgrade. Nothing has been written to the client yet — no status line, no headers — so the first thing your code owes the peer is a complete, well-formed HTTP/1.1 response head telling it the protocol switch was accepted. ## The bytes A 101 response head is a status line, zero or more header lines, and an empty line, all CRLF-terminated: ``` HTTP/1.1 101 Switching Protocols\r\n Upgrade: term\r\n Connection: Upgrade\r\n \r\n ``` Three things routinely go wrong here: 1. **Bare `\n` instead of `\r\n`.** Go's string literals make `\n` the easy thing to type and some peers are lenient, so this fails intermittently rather than immediately — the worst kind of bug to chase. 2. **No terminating blank line.** Without the empty CRLF line the peer is still reading headers and never considers the handshake complete. 3. **No status code constant.** `http.StatusSwitchingProtocols` is 101; using the constant in the string you build keeps it honest, but remember the constant alone writes nothing — you are formatting the line yourself. ## Flush, or the client waits The second return value of `Hijack` is a `*bufio.ReadWriter`, and its writer half is buffered. Bytes you write into it sit in that buffer until it fills or you call `Flush`. A handshake response is well under the buffer size, so *every* hand-rolled upgrade that forgets `Flush` produces the same symptom: the server thinks it answered, the client blocks on a read, and the session hangs until something times it out. Check the error from `Flush` too — that is where a peer that vanished mid-handshake shows up. You may also write the response head straight to the `net.Conn`, which is unbuffered. That is legitimate, but do not mix the two writers without flushing in between, or your bytes can arrive out of order. ## The ResponseWriter is not an option A reasonable-looking alternative is to let net/http write the status for you. It does not work, for a structural reason rather than an API one: as long as the `ResponseWriter` owns the connection, the server owns the framing, the header set and the connection's reuse. Hijack is exactly the transfer of that ownership, and after it the ResponseWriter is inert — writes and `WriteHeader` calls return `http.ErrHijacked` and the client sees nothing. So the sequence is always hijack first, then speak for yourself. ## After the blank line Once the head is flushed, the connection is no longer carrying HTTP. Whatever protocol you have negotiated writes its own bytes on the same socket, in both directions, with no framing help from the standard library. Your handler is now a protocol implementation, and it stays responsible for the connection until it closes it — the server will not. ## Failure paths worth handling If you decide *not* to upgrade (a bad request, a failed authorisation check), decide that **before** hijacking, while the ResponseWriter still works and can send an ordinary 4xx with a body. Once you have hijacked, an error response is one more string you have to format by hand, and any middleware that would have logged or measured the response has already lost its chance to see one.

  • What does the client observe if you skip the Flush after writing the 101?
    Nothing at all. The handshake is a few dozen bytes sitting in the bufio writer, well short of filling it, so no packet leaves the process. The client blocks reading a response that was never sent, and the session hangs until some deadline elsewhere kills it — which, on a hijacked connection, may be never.
  • Can you send the switch with w.WriteHeader instead of hijacking?
    No, because the goal is not the status code but the ownership transfer. While the ResponseWriter holds the connection, net/http still frames the response and manages reuse, so you cannot then treat the socket as a raw stream. And after a hijack the ResponseWriter is inert: WriteHeader returns http.ErrHijacked.
  • Where should the handler reject a bad upgrade request?
    Before calling Hijack. While the ResponseWriter still works you get a normal 4xx with a body, correct framing and whatever logging or metrics middleware wraps the handler. After the hijack, every error response is a string you format by hand and no middleware ever sees a status code.

saying these in an interview costs you the question

  • Writes the handshake with bare newlines instead of CRLF
  • Omits the empty line that terminates the response head
  • Forgets Flush and blames the client for hanging
  • Calls w.WriteHeader(101) after hijacking and expects delivery
  • Decides to reject the request only after hijacking