A WebSocket bootstrapped over HTTP/2 sends no `Sec-WebSocket-Key` and gets no `101`, so which handshake fields survive and what signals success?
answer
- negotiation survives, proof does not
- the digest had a job, now done elsewhere
- look for 200, not 101
- four fields live, two are gone
- lowercase field names on the wire
basics
~10 sOrigin, Sec-WebSocket-Version, Sec-WebSocket-Protocol and Sec-WebSocket-Extensions survive, lowercased as HTTP/2 requires. The Key/Accept digest is gone because the :protocol pseudo-header does its job, and success is a 2xx status - in practice 200.
solid answer
~40 sThe bootstrap keeps the negotiation fields and drops the proof-of-intent ones. `Origin`, `Sec-WebSocket-Version`, `Sec-WebSocket-Protocol` and `Sec-WebSocket-Extensions` all still appear on the extended CONNECT request and mean exactly what they meant in the HTTP/1.1 handshake — written lowercase, because HTTP/2 encodes every field name that way. `Sec-WebSocket-Key` and its `Sec-WebSocket-Accept` answer are not used at all: their job was to prove that a real WebSocket client sent the request and that a real WebSocket server understood it, and `:protocol = websocket` on a method that only an aware server accepts already proves both. Success is a `2xx` on the stream — in practice `:status = 200` — because HTTP/2 has no `101`. An engineer grepping a trace for `101` concludes nothing opened, when in fact everything did.
code
http · 13 lines# request
:method = CONNECT
:protocol = websocket
:scheme = https
:path = /hoist/shaft-3/signals
:authority = hoist-control.example:443
origin = https://console.example
sec-websocket-version = 13
sec-websocket-protocol = hoist.signal.v1
# response
:status = 200
sec-websocket-protocol = hoist.signal.v1go deeper
Remember that over HTTP/2 there is no 101 and no key exchange - a 200 on the stream means the socket is open.
Split the fields into the two that proved intent and the four that negotiate, and explain why only the proving pair became unnecessary.
Watch for version-specific assumptions baked into tooling and dashboards: anything keyed on 101 quietly reports zero the day a fleet moves to HTTP/2.
When a signal changes shape by protocol version, the standard you set is that observability keys on the outcome, not on one version's artifact.
## Two kinds of field, and only one kind survives The HTTP/1.1 handshake carries fields doing two different jobs, and the HTTP/2 bootstrap keeps only one of them. - **Proof fields.** `Sec-WebSocket-Key` (sent by the client) and `Sec-WebSocket-Accept` (returned by the server) are a challenge and its answer. Their purpose was never secrecy — the key travels in clear and the transform is public — it was to prove that a WebSocket-aware client deliberately sent this request and that a WebSocket-aware server deliberately answered it, so that no cache or naive server could be tricked into completing a handshake it did not understand. - **Negotiation fields.** `Origin`, `Sec-WebSocket-Version`, `Sec-WebSocket-Protocol` and `Sec-WebSocket-Extensions` say who is asking, which version of the protocol, and what should be negotiated for the socket. Over HTTP/2 the proof fields are redundant. A `CONNECT` carrying `:protocol = websocket` can only be understood by a server that implements the extension, and a server that does not implement it cannot accidentally complete the exchange — it has no path that leads to a converted stream. The pseudo-header supersedes the digest entirely, so neither `Sec-WebSocket-Key` nor `Sec-WebSocket-Accept` is used. The negotiation fields survive untouched in meaning. ## Lowercase, and why that trips people HTTP/2 carries field names in lowercase. `Sec-WebSocket-Version` goes on the wire as `sec-websocket-version`; `Origin` as `origin`. HTTP/1.1 treated field names case-insensitively, so the mixed-case spellings in `RFC 6455` were always a convention rather than a requirement — but a reader who learned them from a textbook trace and then greps a converted trace for `Sec-WebSocket-Protocol` finds nothing. The values are unchanged. What a negotiated subprotocol name or an extension parameter actually *means* is the same question it was over HTTP/1.1, and is not what changes here. ## What success looks like ```http :status = 200 sec-websocket-protocol = hoist.signal.v1 ``` That is the whole confirmation. The stream is now a tunnel and the WebSocket protocol runs inside it. | | HTTP/1.1 handshake | Extended CONNECT bootstrap | |---|---|---| | Client proof | `Sec-WebSocket-Key` | none - `:protocol` supersedes it | | Server proof | `Sec-WebSocket-Accept` | none | | Version, Origin, subprotocol, extensions | present, mixed case | present, lowercase | | Success status | `101 Switching Protocols` | a `2xx`, in practice `200` | | Field-name casing | case-insensitive | lowercase on the wire | **Say `2xx`, then say `200`.** The specification defines success as a 2xx status on the stream; `200` is what implementations send and what you will see. Promoting that to "it must be exactly 200" is the kind of over-precision an interviewer will push on. ## The failure this causes in practice Someone is debugging a console that will not receive live signals. They take a capture, search it for `101`, find none, and report that the socket never opened. In fact the socket opened on the first attempt, answered `200`, and has been carrying frames for an hour — they were looking for an artifact of a different protocol version. The same reversal happens the other way: a team enables HTTP/2 between two hops, the client silently switches to the bootstrap, and the monitoring that counted `101` responses as "sockets opened" drops to zero. Nothing broke. The metric was version-specific and nobody noticed. The habit worth forming: when you assert anything about a WebSocket handshake, say which HTTP version you are describing. Over HTTP/1.1 it is a `GET`, a key, an accept and a `101`. Over HTTP/2 or HTTP/3 it is a `CONNECT`, a `:protocol`, no digest and a `200`. Both are correct answers to "how does a WebSocket open" and each is wrong about the other.
- Why is dropping the Key/Accept digest safe here, when RFC 6455 treats it as mandatory?The digest proved that both ends were deliberately speaking WebSocket, guarding against a cache or an unaware server completing a handshake by accident. `:protocol = websocket` on an extended CONNECT proves the same thing: only a server that implemented the extension can act on it, and one that did not has no route to a converted stream.
- A monitoring dashboard counts 101 responses as sockets opened. What happens when a client fleet starts using HTTP/2?The count goes to zero while the sockets keep working. The bootstrap answers a 2xx, so a version-specific metric silently stops matching. The fix is to count the thing that is version-independent - established tunnels, or open sockets as the application sees them - not a status code from one HTTP version.
saying these in an interview costs you the question
- Says Sec-WebSocket-Accept still confirms an HTTP/2 bootstrap.
- Looks for 101 in an HTTP/2 trace and reports a failure.
- Thinks all Sec-WebSocket-* fields are dropped over HTTP/2.
- Believes the Key/Accept digest was a security or authentication check.
- Expects mixed-case field names on the HTTP/2 wire.