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:

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

CauseConsole message after "failed:"close event
Nothing listening on the portError in connection establishment: net::ERR_CONNECTION_REFUSED1006, "", wasClean=false
Wrong path, own upgrade handler answers 404Error during WebSocket handshake: Unexpected response code: 4041006, "", false
Wrong path, ws server with path: '/ws'Error during WebSocket handshake: Unexpected response code: 4001006, "", false
Plain HTTP server, no upgrade handler (Node passed the request to the normal handler)Error during WebSocket handshake: Unexpected response code: 2001006, "", false
Proxy that drops the Upgrade and Connection headersError during WebSocket handshake: Unexpected response code: 2001006, "", false
Origin not allowed, server answers 403Error during WebSocket handshake: Unexpected response code: 4031006, "", false
wss:// to a plain ws portError in connection establishment: net::ERR_SSL_PROTOCOL_ERROR1006, "", false
wss:// to a self-signed certificateError in connection establishment: net::ERR_CERT_AUTHORITY_INVALID1006, "", false
ws:// from an HTTPS page (non-local host)Mixed Content error (below); the constructor throwsno events

Two cases opened and then closed. These do not print a console error:

Server action after openclose event in Chromium
ws.close() with no codecode=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:

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:

Causews 8.22.0 client error
Nothing listeningconnect ECONNREFUSED 127.0.0.1:8991 (code ECONNREFUSED)
404 / 200 / 403 responseUnexpected server response: 404 (or 200, 403); an 'unexpected-response' listener gets the full response
wss to a ws portwrite EPROTO … wrong version number (code EPROTO)

11. Fix checklist

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.