A WebSocket bootstrap over HTTP/2 is answered with `:status = 501`, so what has the server said and what should the client do next?
answer
- a refusal of the method, not the socket
- nothing transient about it
- retrying the same stream changes nothing
- the fallback is the older handshake
- unknown :protocol earns a 501
basics
~20 sThe 501 says the server does not support the :protocol the extended CONNECT carried, so no tunnel opened and no WebSocket exists. A client should stop retrying that path and fall back to the RFC 6455 HTTP/1.1 handshake on a separate connection.
solid answer
~40 s`501` is what a server SHOULD return when an extended CONNECT arrives naming a `:protocol` it does not know or does not support. It is a refusal of the bootstrap itself, not a rejection of a subprotocol, a path or a credential — the stream never became a tunnel, so there is no socket at any layer. Retrying the identical request on a new stream produces the same answer, because nothing about it was transient. The useful response is the fallback: open the socket the `RFC 6455` way, over an HTTP/1.1 connection with `Upgrade: websocket`, and remember the refusal for that origin so every later socket skips a doomed round trip. Distinguish this from the other failure: a server that never advertised `SETTINGS_ENABLE_CONNECT_PROTOCOL` should not have been sent an extended CONNECT at all.
code
http · 9 lines# request
:method = CONNECT
:protocol = websocket
:scheme = https
:path = /hoist/shaft-3/signals
:authority = hoist-control.example:443
# response
:status = 501go deeper
Know that a 501 here means the server does not do this kind of bootstrap, and that the older HTTP/1.1 handshake is the way round it.
Explain why the failure is permanent rather than transient, and why no socket exists at any layer after it.
Show the triage: advertisement seen or not, origin or intermediary, and the one test - a socket over a separate HTTP/1.1 connection - that separates the two hypotheses.
Decide the fleet's policy on caching a refusal per origin: too short and every socket pays a round trip, too long and support that arrives is never noticed.
## What the refusal actually means An extended CONNECT answered with `:status = 501` means one thing: the server does not support the protocol named in `:protocol`. The specification says a server **SHOULD** respond this way — it is a recommendation, not an absolute — so a client must also cope with other refusals and with a server that simply errors the stream. What the 501 is **not**: - **Not a subprotocol rejection.** Failing to agree on a `sec-websocket-protocol` value is a different outcome on a tunnel that did open. - **Not a routing or path error.** The server is not saying the resource is missing; it is saying it does not do this kind of request. - **Not transient.** Nothing about the condition will change on the next stream, the next second or the next request. - **Not partial.** There is no half-open socket to clean up. The stream carried a request and a response and that was all. ## Two different failures that look alike from the application From the application's point of view, "the socket did not open" covers two situations with different causes and different fixes. | Situation | What the client saw | What the client should do | |---|---|---| | Server advertised the setting, then refused | `SETTINGS_ENABLE_CONNECT_PROTOCOL` = 1, then a `501` | Fall back to the HTTP/1.1 handshake; record the refusal for this origin | | Server never advertised the setting | no such setting on the connection | Do not send the extended CONNECT at all; go straight to the fallback | The second row is the one clients get wrong. Sending the bootstrap without having seen the advertisement is a client-side protocol error, not an experiment — and the answer you get back tells you nothing you were entitled to ask for. A third situation belongs in the same triage, and it is the one that actually burns engineering time: the request never reached the origin server at all. A hop in the middle that does not implement the extension may refuse the request, may strip what it does not understand, or may fail the stream. Because the class of intermediary, not the origin, decided the outcome, the origin's own logs show nothing and the two ends disagree about what happened. ## Diagnosing it on a link you do not own The hoist console sits at the end of a leased link with hops you cannot inspect. When sockets fail to open, the ordered questions are: 1. **Did the advertisement arrive?** If the setting was never seen on this connection, the client should never have attempted the bootstrap. That is a client bug, and the fallback should have fired already. 2. **Did the refusal come from the far end?** A `501` shaped exactly as the specification describes, arriving with the origin's other responses, suggests the origin. A stream that fails without a status, or a refusal on a connection where ordinary requests succeed, suggests a hop. 3. **Does the HTTP/1.1 path work?** If a socket opens over a separate HTTP/1.1 connection, the socket itself is fine and the bootstrap is what is unsupported. That single test separates "WebSockets are broken here" from "this bootstrap is not available here". ## What a good client does with the answer Cache the verdict per origin, fall back once, and do not re-probe on every socket. A device that re-attempts a refused bootstrap before every connection pays a round trip per socket for information it already has. Equally, do not cache it forever across restarts: server-side support appears when a deployment changes, and a client that decided in 2026 that an origin cannot do this will never notice. ```http # request :method = CONNECT :protocol = websocket :scheme = https :path = /hoist/shaft-3/signals :authority = hoist-control.example:443 # response :status = 501 ``` The absence of any WebSocket field in that response is the tell: nothing was negotiated because nothing was ever going to be.
- How do you tell a 501 from the origin apart from an intermediary refusing the same request?Test whether ordinary requests on the same connection reach the origin and whether a socket opens over a separate HTTP/1.1 connection. If plain requests succeed while every bootstrap is refused, and the origin's logs never record the attempt, a hop in the middle is deciding. Two ends disagreeing about what happened is the signature.
- Should a client keep retrying the extended CONNECT after a 501?No. The condition is not transient, so an immediate retry buys a second refusal. Fall back to the HTTP/1.1 handshake and cache the verdict for that origin so later sockets skip the attempt - but let the cache expire, because server support can appear after a deployment.
saying these in an interview costs you the question
- Treats a 501 on a bootstrap as a transient error worth retrying.
- Reads it as the server rejecting the offered subprotocol.
- Assumes a half-open socket needs cleaning up afterwards.
- Sends the bootstrap without ever seeing the setting advertised.
- Concludes WebSockets are unsupported rather than this bootstrap.