HTTP 101 is Switching Protocols: the server accepts the client’s Upgrade request, and once this response ends, the connection speaks the new protocol (RFC 9110 section 15.2.2). The server must name that protocol in its own Upgrade header and may only pick one the client offered (section 7.8). In practice, a 101 is the WebSocket handshake.
The exchange
RFC 6455’s example:
GET /chat HTTP/1.1
Host: server.example.com
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=
Sec-WebSocket-Accept is the base64 SHA-1 of the key followed by the fixed GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11. It proves the server understood the handshake rather than echoing a 101 at anything. After the blank line, the bytes are WebSocket frames, not HTTP.
When the handshake fails
A browser that gets anything but a 101 fails the WebSocket and fires error and close; your JavaScript never sees the status. Chrome’s console shows it, as in Error during WebSocket handshake: Unexpected response code: 400.
The usual cause is a proxy. Upgrade and Connection are hop-by-hop headers, and nginx drops both when it proxies unless you pass them on. It also talks HTTP/1.0 to upstreams by default, which has no upgrade:
location /ws/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 1h; # the default 60s closes a quiet socket
}
HTTP/2 and HTTP/3 have no 101
RFC 9113 section 8.6 says HTTP/2 doesn’t support 101: one connection carries many streams, so it can’t switch protocols. WebSockets over HTTP/2 use extended CONNECT (RFC 8441) with :protocol set to websocket, and success is a 200; RFC 9220 does the same for HTTP/3. The h2c upgrade from HTTP/1.1 to cleartext HTTP/2 is deprecated too. So a 101 is always HTTP/1.1.
Other rules
- If a request carries both
UpgradeandExpect: 100-continue, the server sends the 100 first, then the 101. Upgradecan’t change the transport. HTTP/3 runs over QUIC, so a client finds it throughAlt-Svc, never through a 101.- Like every 1xx, a 101 is never cached.
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ noServer: true });
server.on('upgrade', (req, socket, head) => { // Node: http.Server's upgrade event
if (req.url !== '/ws/') return socket.destroy();
wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req));
});
Search 101 426 in the tool below to compare it with Upgrade Required.