WebSocket connection failed: Error during WebSocket handshake: Unexpected response code: 400

Problem solved! I just figured out how to solve the issue, but I would still like to know if this is normal behavior or not.

It seems that even though the Websocket connection establishes correctly (indicated by the 101 Switching Protocols request), it still defaults to long-polling. The fix was as simple as adding this option to the Socket.io connection function:

{transports: ['websocket']}

So the code finally looks like this:

const app = express();
const server = http.createServer(app);
var io = require('socket.io')(server);

io.on('connection', function(socket) {
  console.log('connected socket!');

  socket.on('greet', function(data) {
    console.log(data);
    socket.emit('respond', { hello: 'Hey, Mr.Client!' });
  });
  socket.on('disconnect', function() {
    console.log('Socket disconnected');
  });
});

and on the client:

var socket = io('ws://localhost:3000', {transports: ['websocket']});
socket.on('connect', function () {
  console.log('connected!');
  socket.emit('greet', { message: 'Hello Mr.Server!' });
});

socket.on('respond', function (data) {
  console.log(data);
});

And the messages now appear as frames:

working websockets

This Github issue pointed me in the right direction. Thanks to everyone who helped out!


This worked for me with Nginx, Node server and Angular 4

Edit your nginx web server config file as:

server {
listen 80;
server_name 52.xx.xxx.xx;

location / {
    proxy_set_header   X-Forwarded-For $remote_addr;
    proxy_set_header   Host $http_host;
    proxy_pass         "http://127.0.0.1:4200";
    proxy_http_version 1.1;
    proxy_set_header   Upgrade $http_upgrade;
    proxy_set_header   Connection "upgrade";
}

The currently accepted solution is misleading.

According to the official documentation, adding the transports: [ 'websocket' ] option effectively removes the ability to fallback to long-polling when the websocket connection cannot be established. This option is what makes socket.io so robust in the first place because it can adapt to many scenarios.

In that particular case where one wishes to solely rely on websockets, directly using the WebSocket API is recommended.

For other cases (supposedly most users), this is most likely a reverse proxy/server configuration problem.

The official documentation suggests the following depending on your environment:

NginX configuration

http {
  server {
    listen 3000;
    server_name io.yourhost.com;

    location / {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $host;

      proxy_pass http://nodes;

      # enable WebSockets
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
    }
  }

  upstream nodes {
    # enable sticky session based on IP
    ip_hash;

    server app01:3000;
    server app02:3000;
    server app03:3000;
  }
}

Apache HTTPD configuration

Header add Set-Cookie "SERVERID=sticky.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED

<Proxy "balancer://nodes_polling">
    BalancerMember "http://app01:3000" route=app01
    BalancerMember "http://app02:3000" route=app02
    BalancerMember "http://app03:3000" route=app03
    ProxySet stickysession=SERVERID
</Proxy>

<Proxy "balancer://nodes_ws">
    BalancerMember "ws://app01:3000" route=app01
    BalancerMember "ws://app02:3000" route=app02
    BalancerMember "ws://app03:3000" route=app03
    ProxySet stickysession=SERVERID
</Proxy>

RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) balancer://nodes_ws/$1 [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /(.*) balancer://nodes_polling/$1 [P,L]

ProxyTimeout 3

HAProxy configuration

listen chat
  bind *:80
  default_backend nodes

backend nodes
  option httpchk HEAD /health
  http-check expect status 200
  cookie io prefix indirect nocache # using the `io` cookie set upon handshake
  server app01 app01:3000 check cookie app01
  server app02 app02:3000 check cookie app02
  server app03 app03:3000 check cookie app03

Also worth reading this on upgrading connections in HAProxy.

For more details please refer to the official documentation link above.

EDIT:

Varnish (source here)

sub vcl_recv {
    if (req.http.upgrade ~ "(?i)websocket") {
        return (pipe);
    }
}

sub vcl_pipe {
    if (req.http.upgrade) {
        set bereq.http.upgrade = req.http.upgrade;
        set bereq.http.connection = req.http.connection;
    }
}