How to test websocket with curl
Run curl -sv --max-time 2 ws://host/path on a build whose Protocols line lists ws. The verbose log prints the upgrade request, the Sec-WebSocket-Key curl generated, and the 101 response with its Sec-WebSocket-Accept. A build without ws answers curl: (1) Protocol "ws" not supported.
Why check this
Run this when a socket fails and the client library reports a network error with no status code, and whenever a new proxy, gateway or TLS terminator lands in front of a WebSocket service. curl prints both halves of the upgrade, so it is the client that tells you what an intermediary changed.
The failure it prevents is a misread blame line. A load balancer that answers the upgrade itself returns a valid 101 and forwards nothing, and client libraries report that as a working connection until frames fail to arrive. curl shows the key it sent next to the accept value that came back, the one field an intermediary cannot fake without doing the hash.
Prerequisites
- A curl with WebSocket support. Check the
Protocolsline, as step 1 does. On Windows,winget install cURL.cURLgives 8.21.0; the curl that ships with Git Bash is 8.1.2 and has nowsat all. Step 2 shows what that build says. - openssl for the accept check. This page used 3.1.1.
- A local echo server, so no third-party host is touched. In an empty folder,
npm i ws@8, then:
// echo-server.mjs node 22, npm i ws@8
import { createServer } from 'node:http';
import { WebSocketServer } from 'ws';
const server = createServer((req, res) => {
res.writeHead(426, { 'content-type': 'text/plain', 'sec-websocket-version': '13' });
res.end('this endpoint speaks WebSocket only\n');
});
const wss = new WebSocketServer({ server, path: '/ws' });
wss.on('connection', (ws) => ws.on('message', (d) => ws.send('echo:' + d)));
server.listen(19317, '127.0.0.1', () => console.log('ws://127.0.0.1:19317/ws'));
- For the
wss://step, a certificate and the same server behind TLS:
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 7 -subj "//CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
// tls-server.mjs the same echo server behind TLS, self-signed certificate
import { createServer } from 'node:https';
import { readFileSync } from 'node:fs';
import { WebSocketServer } from 'ws';
const server = createServer({ key: readFileSync('key.pem'), cert: readFileSync('cert.pem') });
const wss = new WebSocketServer({ server, path: '/ws' });
wss.on('connection', (ws) => ws.on('message', (d) => ws.send('echo:' + d)));
server.listen(19318, '127.0.0.1', () => console.log('wss://127.0.0.1:19318/ws'));
Steps
- Step 1.
Read the protocol list of the curl you are about to use.
curl --version | sed -n '1p;3p'curl 8.21.0 (x86_64-w64-mingw32) libcurl/8.21.0 LibreSSL/4.3.2 zlib/1.3.1.zlib-ng brotli/1.2.0 zstd/1.5.7 WinIDN libpsl/0.23.0 libssh2/1.11.1 nghttp2/1.70.0 ngtcp2/1.25.0 nghttp3/1.18.0 WinLDAP Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt mqtts pop3 pop3s rtsp scp sftp smtp smtps telnet tftp ws wsswsandwssat the end of the second line are the whole prerequisite. Support is compiled in, not switched on at runtime, so a build without them cannot be persuaded. - Step 2.
Run the same URL on a build that has no WebSocket support, so you recognise the message. This one is the Git Bash curl 8.1.2, whose
Protocolsline ends attftp.curl -sS ws://127.0.0.1:19317/wscurl: (1) Protocol "ws" not supported or disabled in libcurlExit code 1 and no request on the wire, so nothing about the endpoint has been tested. Install a newer build, or read the handshake over
http://, which any curl can do. - Step 3.
Open the socket and read the upgrade in full.
curl -sv --max-time 2 ws://127.0.0.1:19317/ws* Trying 127.0.0.1:19317... * Established connection to 127.0.0.1 (127.0.0.1 port 19317) from 127.0.0.1 port 56772 * using HTTP/1.x > GET /ws HTTP/1.1 > Host: 127.0.0.1:19317 > User-Agent: curl/8.21.0 > Accept: */* > Upgrade: websocket > Sec-WebSocket-Version: 13 > Sec-WebSocket-Key: BkYEQeYfyNnokoQ53dE5NQ== > Connection: Upgrade > * Request completely sent off < HTTP/1.1 101 Switching Protocols < Upgrade: websocket < Connection: Upgrade < Sec-WebSocket-Accept: e5dKGeXfFTXhv34Y3c7ADXht4Bg= < * Received 101, Switching to WebSocket * [WS] Received 101, switch to WebSocket * Operation timed out after 2008 milliseconds with 0 bytes received * closing connection #0curl built the upgrade request itself: the scheme chose
Upgrade: websocket, version 13 and a fresh random key. The timeout on the last line is not a failure. The socket was open and idle, because curl has no frame to send. - Step 4.
Recompute the accept value from the key in the request above. The server appends the fixed GUID
258EAFA5-E914-47DA-95CA-C5AB0DC85B11, hashes with SHA-1 and encodes base64.printf '%s' 'BkYEQeYfyNnokoQ53dE5NQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11' | openssl sha1 -binary | openssl base64e5dKGeXfFTXhv34Y3c7ADXht4Bg=It matches the header in step 3, so the peer that answered ran the WebSocket handshake rather than returning a 101 from a rule. The key changes on every run, so copy the one from your own output.
- Step 5.
Repeat over TLS. The local certificate is self-signed, so
-kis needed; against a real endpoint, leave it out and let verification run.curl -sv --max-time 3 -k wss://127.0.0.1:19318/ws* Trying 127.0.0.1:19318... … * SSL Trust: peer verification disabled … * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / [blank] / UNDEF * Server certificate: * subject: CN=localhost * start date: Sep 11 22:43:29 2026 GMT * expire date: Sep 18 22:43:29 2026 GMT * issuer: CN=localhost … * OpenSSL verify result: 12 * SSL certificate verification failed, continuing anyway! … > GET /ws HTTP/1.1 … > Upgrade: websocket > Sec-WebSocket-Version: 13 > Sec-WebSocket-Key: ERBaCza/EGMzhnvtlY7KJw== > Connection: Upgrade … < HTTP/1.1 101 Switching Protocols < Upgrade: websocket < Connection: Upgrade < Sec-WebSocket-Accept: KV+sz/gVkE8nG/xte078hunCBDM= … * [WS] Received 101, switch to WebSocket * Operation timed out after 3008 milliseconds with 0 bytes received * closing connection #0TLS finishes before the upgrade starts, so a
wss://failure is usually a certificate problem rather than a WebSocket one. Without-kthe same command stops atverify result: self signed certificate (18)and never sends the GET. - Step 6.
Try to send a frame with
-d, and watch curl ignore it.curl -sv --max-time 3 ws://127.0.0.1:19317/ws -d 'hello from curl'* Trying 127.0.0.1:19317... * Established connection to 127.0.0.1 (127.0.0.1 port 19317) from 127.0.0.1 port 60847 * using HTTP/1.x > GET /ws HTTP/1.1 > Host: 127.0.0.1:19317 > User-Agent: curl/8.21.0 > Accept: */* > Upgrade: websocket > Sec-WebSocket-Version: 13 > Sec-WebSocket-Key: kLnhaAtZ4YT96OJr7MNEng== > Connection: Upgrade > * Request completely sent off < HTTP/1.1 101 Switching Protocols < Upgrade: websocket < Connection: Upgrade < Sec-WebSocket-Accept: wDNuiBjd4FYqqCvkRK7F9473PKY= < * Received 101, Switching to WebSocket * [WS] Received 101, switch to WebSocket * Operation timed out after 3013 milliseconds with 0 bytes received * closing connection #0The request is still a GET with no body, and the echo server answered nothing, because no frame arrived. No warning is printed. For a round trip, use a client that writes frames, such as
npx wscat@6.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| [WS] Received 101, switch to WebSocket | The upgrade completed | Verify the accept value, then send frames with another client |
| curl: (1) Protocol "ws" not supported | Your curl has no WebSocket support | Install 8.21.0 or read the handshake over http:// |
| 101 with an accept value that fails step 4 | Something answered 101 without doing the hash | Suspect the proxy or gateway, not the application |
| The response is 200, 404 or 426 | The upgrade never reached a WebSocket route | Check the path and whether the Upgrade header survives |
| curl: (28) after the 101 | The socket is open and idle | Expected. curl cannot send a frame to provoke a reply. |
| verify result: self signed certificate (18) | TLS failed before the upgrade | Fix the certificate first. The socket is not the problem. |
Common mistakes
What to check next
- Websocket handshake failed: every failure status this page can return, read field by field.
- How to test websocket connection: the frame round trip curl cannot do.
- How to check if websocket is secure: what to verify on the TLS side of step 5.
- Websocket authentication jwt: sending a token on the upgrade with
-H. - How to check websocket connection in chrome: the same upgrade as the browser records it.
FAQ
Can curl send and receive WebSocket messages?
The library can. The command line tool opens the connection and prints what arrives, with no option for writing a frame, as step 6 shows. Use wscat for a round trip.
How to test wss with curl?
Same command with the wss:// scheme. TLS is verified first, so a self-signed certificate stops the run before the upgrade. Add -k only against a local endpoint.
How do I test a websocket connection with curl on an old build?
Send the handshake yourself over http:// with -H 'Connection: Upgrade', -H 'Upgrade: websocket', a key and a version. Any build does that, because it never touches the WebSocket code.
Why does curl always report a timeout?
Because a WebSocket server sends nothing until it is spoken to, and curl never speaks. Use --max-time to end the run, and read the 101 above it.
Does a 101 from curl prove the endpoint works?
It proves the handshake works. Recompute the accept value to prove the peer ran the hash, then send a frame with another client to prove the application is listening.
Verified
Verified by Maks Vernycurl 8.21.0curl 8.1.2openssl 3.1.1node 22.23.2ws 8.21.3
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate7 minpublished updated Maks Verny