WebSocket tester
Open a WebSocket from your browser, send frames and read what comes back. Connects straight to your server, so localhost works. Explains close code 1006 rather than printing it.
Runs in your browser
Frames
- Nothing yet.
Your browser connects straight to the address you enter. Nothing goes through this site, which is why a local address works.
Close codes
| Code | Meaning |
|---|---|
| 1000 | Normal closure |
| 1001 | Going away |
| 1002 | Protocol error |
| 1003 | Unsupported data |
| 1005 | No status receivednever sent by a server |
| 1006 | Abnormal closurenever sent by a server |
| 1007 | Invalid frame payload data |
| 1008 | Policy violation |
| 1009 | Message too big |
| 1010 | Mandatory extension missing |
| 1011 | Internal server error |
| 1012 | Service restart |
| 1013 | Try again later |
| 1014 | Bad gateway |
| 1015 | TLS handshake failednever sent by a server |
It connects from your browser, not from this site
The socket is opened by your own browser, directly to the address you type. Nothing routes through this site, which has two consequences worth knowing:
ws://localhost:8080works. A server on your machine, or on your office network, is reachable here even though no server on the internet could reach it. Testers that proxy the connection through their own backend cannot do this.- Nothing you send is visible to anyone but your server. There is no third party in the path because there is no third party in the design.
Close code 1006, which is why most people are here
1006 is not a message from your server. Your server cannot send it — the spec forbids it, along
with 1005 and 1015. It is what the browser reports when the connection ended without a
close frame, and it means only that: the socket died and no one said why.
Something in this list, and the WebSocket API will not tell you which:
- Nothing was listening on that host and port.
- TLS failed — an expired, self-signed or wrong-hostname certificate on
wss://. - A proxy or load balancer between you and the server did not forward the upgrade.
- The server process died, or a network dropped mid-connection.
- The server rejected the handshake with an HTTP status instead of
101. - You offered a subprotocol and the server did not echo one back — see below.
The API hides the reason on purpose. A page must not be able to distinguish “connection refused” from “no route to host”, because that difference is enough to port-scan the visitor’s private network from a web page. You are not being denied information the browser has; the browser is deliberately not passing it on.
The browser’s own console usually has the real error, and it is not reachable from JavaScript. Open devtools, look at the console and the network panel’s WS tab, and you will often find the HTTP status the handshake actually got.
Mixed content: the failure that happens before any traffic
This page is served over HTTPS, and a secure page may not open a plain ws:// socket. The
browser blocks it before a single byte leaves — and reports it as an ordinary connection failure,
so it looks exactly like a server that is down.
This page checks for it before opening the socket and says so plainly instead.
localhost is exempt: browsers treat loopback as a secure context, so testing a local dev server
over ws:// from an HTTPS page works. Anything else needs wss://.
The handshake, and why a proxy breaks it
A WebSocket starts as an ordinary HTTP GET carrying Upgrade: websocket and
Connection: Upgrade, and succeeds only if the server answers 101 Switching Protocols. Any
other status ends it.
Almost every “works locally, fails in production” WebSocket problem is a reverse proxy that strips those headers. nginx needs them restored explicitly:
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
proxy_http_version 1.1 matters — the default is 1.0, which has no upgrade mechanism. The
timeout matters too: nginx closes an idle connection after 60 seconds by default, which presents
as a socket that works fine and then dies exactly a minute later.
Subprotocols
The optional second field sends a Sec-WebSocket-Protocol header offering a list, in preference
order. The server picks one and echoes it back; the page shows which. graphql-ws, mqtt,
json are the ones you will meet.
Offering one the server does not echo back kills the connection, and this catches people out.
A server that does not recognise any of them completes the handshake without the header — and
RFC 6455 then requires the client to fail the connection. The browser does exactly that, and
you get 1006, the same code a server that was never running would give you.
So a socket that dies instantly with a subprotocol filled in, and connects fine with the field empty, is not a broken server. It is a server that does not speak the subprotocol you asked for.
You cannot set headers, and that shapes authentication
The browser WebSocket API takes a URL and a subprotocol list. That is all. There is no way to
send an Authorization header — not here, not in any browser, no matter what library you use.
So server authentication has to arrive some other way:
- A cookie, sent automatically for the socket’s origin. Simple, and subject to the usual CSRF-ish caveats since WebSocket handshakes are not covered by CORS.
- A token in the query string,
wss://host/ws?token=…. Widely used, and it lands in access logs and proxy logs. Use short-lived tokens. - A first message after the socket opens, with the server refusing to do anything until it arrives. The most work and the cleanest.
Some libraries appear to send an Authorization header from the browser. They are not; they are
sending it in the first frame.
Close codes
The table above lists them. The shape worth remembering:
- 1000–1015 are defined by the protocol.
1000is a normal close,1001is a page or server going away,1009is a frame over the size limit,1011is a server error. - 3000–3999 are registered with IANA by libraries and frameworks.
- 4000–4999 are yours. If you want a close to mean something specific to your application, this is the range to use — nothing else will ever collide with it.
A browser may only send 1000 or a code in 3000–4999. Disconnecting from this page sends 1000.
Related
What is my IP for what your server sees, CSR generator
for the certificate behind wss://, and JSON formatter for reading the
frames once they arrive.