skip to content

A reverse proxy now sits in front of your WebSocket service and clients get 200 OK with an HTML body instead of 101. What happened?

level: seniorimportance: must knowfreq 64%

answer

  1. the response is well-formed, not broken
  2. the origin never saw an upgrade
  3. connection-scoped, consumed per hop
  4. re-issue both fields upstream
  5. tunnel bytes once the 101 returns

basics

~20 s

The proxy did not forward the Upgrade and Connection fields. They are connection-scoped, meaningful only on the hop that carries them, so the origin saw a plain GET for that path and answered it normally with the page it serves there.

solid answer

~40 s

`Upgrade: websocket` and `Connection: Upgrade` are hop-by-hop fields: they apply to a single connection, and an intermediary is expected to consume them rather than blindly copy them upstream. A proxy that has not been configured to re-issue them therefore forwards an ordinary `GET`. Your origin never sees an upgrade request, so it does nothing special — it serves that path and returns a complete `200 OK` with whatever body lives there. The client was waiting for `101 Switching Protocols`, gets a normal response instead, and fails the handshake. The fix is on the intermediary: re-issue both fields upstream, speak HTTP/1.1 on that hop, and stop parsing HTTP and start relaying bytes once the `101` comes back.

code

http · 11 lines
http
GET /occupancy HTTP/1.1
Host: board.example
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

go deeper

for a junior

Know that a successful WebSocket handshake ends in 101 Switching Protocols, so any ordinary response — a page, a redirect, a gateway error — means no socket was ever created.

for a middle

Explain that Upgrade and Connection apply only to the hop that carries them, so an intermediary must be configured to re-create them upstream rather than copy them along.

for a senior

Separate the failure modes by mechanism: fields dropped gives an ordinary response, a hop that cannot tunnel gives a gateway error, and an ordinary idle allowance kills a healthy socket minutes later.

for a principal

Treat upgrade support as a property of the whole request path and require it of every hop the platform adds, so a new edge layer cannot silently degrade every long-lived connection in the estate.

## Read the symptom precisely The client sent a WebSocket handshake and got back a complete, well-formed HTTP response: `200 OK`, a content type, a length, a body. That is not a timeout, not a reset, and not a refused connection. Something answered the request *as a request*. Either the intermediary answered it, or it forwarded something that no longer looked like an upgrade — and the second is by far the more common. ## Why the fields disappear by design `Upgrade` and `Connection` are **connection-scoped**: they describe what is to happen on the one hop they arrived on, not an instruction addressed to the far end of a chain. `Connection` is itself the field that designates which other fields are scoped that way, and on a WebSocket handshake it designates `Upgrade`. An intermediary is therefore supposed to consume them and make its own decision about its own upstream connection. So the default behaviour of a general-purpose intermediary is not a bug — it is the rule being followed. Forwarding a handshake means *deliberately re-creating* it on the next hop, which is why proxying a socket is always a configuration and never an accident: 1. re-issue `Upgrade: websocket` and `Connection: Upgrade` on the upstream request; 2. speak HTTP/1.1 upstream — an HTTP/1.0 hop has no upgrade mechanism, so the handshake cannot survive it; 3. when `101 Switching Protocols` comes back, stop treating the connection as a request/response exchange and relay raw bytes in both directions until either side closes; 4. give that connection a far longer idle allowance than an ordinary request, because a healthy socket may carry nothing for minutes. Step 3 is where the second class of failure lives. An intermediary that forwards the fields but does not know what to do with a `101` may tear the connection down or report a gateway error instead of tunnelling. ## The three shapes of the same misconfiguration | what the client sees | what happened | |---|---| | `200 OK` with a page body | the fields were dropped, the origin answered the path as an ordinary GET | | `404` or a redirect | the fields were dropped **and** the path is not one the origin serves normally | | `502 Bad Gateway` | the fields arrived, but the hop could not carry or interpret the upgrade upstream | | handshake completes, then dies quietly seconds later | the tunnel was established but the hop applies an ordinary request idle allowance | The first three are all visible in the handshake, which makes them cheap to diagnose: capture the exact request the origin received. If `Upgrade` and `Connection` are missing there but present in what the client sent, you have your answer in one line and the change is on the intermediary, not in your service. ## Why this is misdiagnosed The response body is a page, so the first instinct is a routing problem — wrong host, wrong path, wrong virtual server. The routing is usually fine. The request reached exactly the handler it was aimed at; that handler simply received a request with no upgrade in it and did the only thing it could. A second instinct is to blame encryption: "the hop is TLS-terminated, so the intermediary could not read the fields." Terminating TLS is precisely what lets it read them — an intermediary that terminates reads and re-creates the whole request, and the fields are dropped in that re-creation. ## On the occupancy board The board's socket runs through an edge terminator, a caching layer and a municipal filtering appliance. Each is a hop, and each has to be told to carry the upgrade; one that has not been will silently degrade the handshake into an ordinary request and hand the board back its own dashboard page. The useful engineering habit is to treat the handshake as an end-to-end contract you test per hop: send the upgrade at each boundary in turn and check for `101 Switching Protocols`. The first hop that answers `200 OK` is the one holding the configuration you are missing, and the rest of the chain is innocent. A final operational note: because the upgrade rides a plaintext scheme so much more often on internal hops, the hop most likely to mangle the handshake is the one where the bytes are inspectable. A hop that tunnels an encrypted connection has nothing to re-create and nothing to drop.

  • The proxy forwards both fields correctly but clients still drop about a minute after connecting. What now?
    The tunnel is established and something is applying an ordinary request idle allowance to it. A socket that carries no bytes for a minute looks idle to a hop that does not know it is a socket, so it cuts the connection with no Close control frame. Raise the idle allowance on that hop for the socket path.
  • How do you find which hop in a chain is dropping the upgrade?
    Send the handshake at each boundary in turn and look for `101 Switching Protocols`. The first hop that answers an ordinary response instead is the one missing the configuration. Comparing the request the client sent with the request the origin logged also shows the two missing fields directly.
  • Why does an HTTP/1.0 upstream hop break the handshake even when the fields are forwarded?
    The upgrade mechanism is an HTTP/1.1 feature; there is no way to express it on a 1.0 request and no `101 Switching Protocols` to come back. The hop must speak HTTP/1.1 upstream for the exchange to exist at all.

A courier told at a junction to switch to the express lane. The instruction is for that junction only — a courier who does not act on it just keeps driving, and the recipient gets an ordinary delivery with no sign anything was ever asked for.

saying these in an interview costs you the question

  • Diagnoses it as a routing or wrong-path problem
  • Claims TLS termination hid the fields from the intermediary
  • Thinks an intermediary forwards all request fields unchanged
  • Expects the origin to detect a socket client without the upgrade fields
  • Assumes the proxy keeps parsing HTTP after the 101 instead of relaying bytes
  • Applies the ordinary request idle allowance to a tunnelled socket