WebSocket connection to 'ws://...' failed (tested)
WebSocket connection to 'ws://…' failed is Chromium's console error when a WebSocket cannot open. The text after failed: tells you which cause it is. We reproduced nine causes against a local Node server and recorded the exact console line, the close event and what Node 24 reports for the same thing.
1. Read the part after "failed:"
Chromium writes one console line per failure. There are two families:
Error in connection establishment: net::ERR_…— the TCP or TLS connection never worked. The server never saw an HTTP request.Error during WebSocket handshake: Unexpected response code: NNN— the server answered, but with status NNN instead of101 Switching Protocols.
Your JavaScript gets much less. In every failure we tested, the page saw an error event and then a close event with code=1006, an empty reason and wasClean=false. The status code and the net error are only in the console. Browsers hide them from scripts on purpose.
2. Our test setup
A Node 24 server on 127.0.0.1 used the ws package in noServer mode. Its 'upgrade' handler accepted /ws, answered 404 for unknown paths, 200 for /200, 403 for /origin unless the Origin was on an allow list, and had paths that close right after opening. A second port ran, one at a time: nothing, a ws server with path: '/ws', a plain HTTP server with no upgrade handler, a naive proxy, and an HTTPS server. Headless Chromium 153 opened each URL with new WebSocket(url) from a page on the same box.
3. Results in Chromium 153
| Cause | Console message after "failed:" | close event |
|---|---|---|
| Nothing listening on the port | Error in connection establishment: net::ERR_CONNECTION_REFUSED | 1006, "", wasClean=false |
| Wrong path, own upgrade handler answers 404 | Error during WebSocket handshake: Unexpected response code: 404 | 1006, "", false |
Wrong path, ws server with path: '/ws' | Error during WebSocket handshake: Unexpected response code: 400 | 1006, "", false |
| Plain HTTP server, no upgrade handler (Node passed the request to the normal handler) | Error during WebSocket handshake: Unexpected response code: 200 | 1006, "", false |
| Proxy that drops the Upgrade and Connection headers | Error during WebSocket handshake: Unexpected response code: 200 | 1006, "", false |
| Origin not allowed, server answers 403 | Error during WebSocket handshake: Unexpected response code: 403 | 1006, "", false |
wss:// to a plain ws port | Error in connection establishment: net::ERR_SSL_PROTOCOL_ERROR | 1006, "", false |
wss:// to a self-signed certificate | Error in connection establishment: net::ERR_CERT_AUTHORITY_INVALID | 1006, "", false |
ws:// from an HTTPS page (non-local host) | Mixed Content error (below); the constructor throws | no events |
Two cases opened and then closed. These do not print a console error:
| Server action after open | close event in Chromium |
|---|---|
ws.close() with no code | code=1005, reason "", wasClean=true |
ws.close(4000, 'go away') | code=4000, reason "go away", wasClean=true |
ws.terminate() (socket destroyed, no close frame) | code=1006, reason "", wasClean=false |
So 1006 alone does not tell you whether the handshake failed or an open socket died. Check whether open fired first.
4. Connection refused
Nothing is listening on that host and port. The server is down, on another port, or bound to a different interface. Test the port with curl first. See ERR_CONNECTION_REFUSED / ECONNREFUSED for the full checklist.
5. Unexpected response code: 404, 400 or 200
The server got the request but did not treat it as a WebSocket upgrade. A working handshake looks like this with curl:
$ curl -i -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
http://127.0.0.1:8990/ws
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
The same request to a wrong path got HTTP/1.1 404 Not Found. What each code usually means:
- 404: wrong path, or your upgrade handler rejects unknown paths. Compare the client URL with the server route character by character, including a trailing slash.
- 400: the
wspackage's server with apathoption answered 400 to a different path in our test. Other servers also use 400 for a bad handshake. - 200: the request reached a normal HTTP handler. Either no WebSocket server is attached, or a proxy in front removed the
Upgradeheader. In our proxy test the backend logged the request with noUpgradeheader and sent its normal page.
6. Proxy drops the Upgrade header
Upgrade and Connection are hop-by-hop headers. A proxy does not forward them unless told to. Our naive Node proxy deleted them and forwarded the rest; Chromium then saw a plain 200. nginx needs this in the WebSocket location (per the nginx docs, not measured):
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 3600s;
}
Per the nginx docs, the default proxy_read_timeout is 60 s, so an idle socket is closed after a minute unless you raise it or send pings.
7. Origin rejected (403)
Browsers always send an Origin header on the WebSocket handshake. Our server logged http://127.0.0.1:8990 for the test page. If the server checks it against an allow list, a page on a new domain gets 403. Add the exact origin (scheme, host and port) to the list. CORS headers do not apply to WebSockets; the server must check Origin itself. In Node 24, the built-in client let us set headers: { origin: 'https://allowed.example' } and the same endpoint opened.
8. ws:// from an HTTPS page
From https://page.test:8991/, new WebSocket('ws://api.test:8990/ws') threw right away:
SecurityError: Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.
The console said: Mixed Content: The page at 'https://page.test:8991/' was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint 'ws://api.test:8990/ws'. This request has been blocked; this endpoint must be available over WSS.
Use wss://, or build the URL from the page: (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws'. One catch for local testing: ws://127.0.0.1 from the same HTTPS page was allowed and opened, because Chromium treats loopback as secure. So the bug can hide on localhost and appear in production. More in Mixed Content blocked.
9. wss:// to a plain ws port
Chromium gave net::ERR_SSL_PROTOCOL_ERROR. The server spoke plain HTTP to a TLS hello. Either terminate TLS in front (nginx, a load balancer) and point wss:// there, or start the server with HTTPS. See ERR_SSL_PROTOCOL_ERROR. A self-signed certificate gives net::ERR_CERT_AUTHORITY_INVALID instead; see ERR_CERT_AUTHORITY_INVALID.
10. Node.js clients tell you less (built-in) or more (ws)
Node 24's built-in WebSocket gave the same close events as Chromium (1006 / 1005 / 4000, same wasClean values). But for every failure, the error event was an ErrorEvent with an empty message, and event.error was a bare TypeError with an empty message and no cause. Refused, 404, 403 and wss-to-ws all looked identical. A missing error listener did not crash the process. A bad scheme threw SyntaxError: expected a ws: or wss: url; http:// URLs were accepted and opened.
The ws package client is more useful for debugging:
| Cause | ws 8.22.0 client error |
|---|---|
| Nothing listening | connect ECONNREFUSED 127.0.0.1:8991 (code ECONNREFUSED) |
| 404 / 200 / 403 response | Unexpected server response: 404 (or 200, 403); an 'unexpected-response' listener gets the full response |
| wss to a ws port | write EPROTO … wrong version number (code EPROTO) |
11. Fix checklist
- Open DevTools Console and read the text after
failed:. Youronerrorhandler will not have it. ERR_CONNECTION_REFUSED: start the server, check host, port and bind address.- 404 / 400: fix the path.
- 200: attach the WebSocket server, and make every proxy forward
UpgradeandConnection. - 403: add the page's exact origin to the server's allow list.
- Mixed Content / SecurityError: use
wss://on HTTPS pages. ERR_SSL_PROTOCOL_ERROR: you usedwss://on a port without TLS.- Opens then 1006: the server or a proxy killed the socket. Check idle timeouts and send pings.
Frequently asked questions
Why is close code 1006 with an empty reason?
1006 means the connection closed without a close frame. In Chromium 153 and Node 24, every failed handshake and every killed socket gave 1006, wasClean=false and an empty reason. The real cause is only in the browser console.
Can I read the HTTP status of a failed WebSocket in JavaScript?
Not in the browser. Chromium printed 'Unexpected response code: 404' in the console, but the page only got an error event and close code 1006. In Node, the ws package exposes it via the 'unexpected-response' event.
Why does my WebSocket get 'Unexpected response code: 200'?
The request was served as normal HTTP. In our tests this happened with no upgrade handler on the server and with a proxy that removed the Upgrade header. Fix the server or set proxy_set_header Upgrade and Connection in nginx.
Why does ws:// work on localhost but fail in production?
Chromium 153 allowed ws://127.0.0.1 from an HTTPS page because loopback counts as secure. A non-local host was blocked as Mixed Content with a SecurityError. Use wss:// on HTTPS pages.
Does CORS affect WebSockets?
No. The browser does not run CORS checks on WebSockets. It sends an Origin header, and the server must check it. A server that rejected our origin answered 403, which Chromium showed as 'Unexpected response code: 403'.
Written with AI assistance. Measured on 2026-10-02 with headless Chromium 153.0.8010.12 (Playwright), Node.js 24.21.0 (built-in WebSocket), the ws package 8.22.0 (server and client) and curl 8.14.1, against local Node servers. The nginx config is from the nginx docs, not measured. PlainHeaders is our extension; Chromium, Node.js, ws and curl are free.