toolfree

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

Closed
State
Subprotocol agreed

Frames

  1. 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

CodeMeaning
1000Normal closure
1001Going away
1002Protocol error
1003Unsupported data
1005No status receivednever sent by a server
1006Abnormal closurenever sent by a server
1007Invalid frame payload data
1008Policy violation
1009Message too big
1010Mandatory extension missing
1011Internal server error
1012Service restart
1013Try again later
1014Bad gateway
1015TLS 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:

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:

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:

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:

A browser may only send 1000 or a code in 3000–4999. Disconnecting from this page sends 1000.

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.