Nginx WebSocket Reverse Proxy Returns 200 Instead of 101: Configuration Fixes and Debugging Steps


10 views

When implementing WebSocket connections through Nginx, the critical response code we expect is 101 Switching Protocols. This indicates successful WebSocket protocol negotiation. The 200 status suggests Nginx is handling the request as a standard HTTP request rather than upgrading to WebSocket.

For proper WebSocket proxying, Nginx needs these essential directives:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

After extensive testing, here's the verified configuration that handles both WebSocket and regular HTTP traffic:

server {
    listen 80;
    server_name chat.example.com;

    # Standard HTTP traffic
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # WebSocket endpoint
    location /ws/ {
        proxy_pass http://127.0.0.1:6060;
        proxy_http_version 1.1;
        proxy_set_header Upgrade "websocket";
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Several factors can prevent the WebSocket upgrade:

  • Using HTTP/1.0 instead of 1.1
  • Missing or incorrect Upgrade/Connection headers
  • Corporate proxies intercepting WebSocket traffic
  • CORS issues when client and server origins differ

Use these tools to debug WebSocket issues:

# Check raw headers with curl
curl -i -H "Connection: Upgrade" -H "Upgrade: websocket" http://chat.example.com/ws/

# Monitor traffic between Nginx and backend
ngrep -d lo -W byline port 6060

For production environments, consider adding:

proxy_read_timeout 86400s; # WebSocket connection timeout
proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key;
proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version;

When setting up a real-time chat application using hack.chat with Nginx as a reverse proxy, many developers encounter a crucial issue: the WebSocket connection returns HTTP status code 200 instead of the expected 101 (Switching Protocols). This prevents the WebSocket connection from being properly established.

The original Nginx configuration looked like this:

map $http_upgrade $connection_upgrade{
    default upgrade;
    ''  close;
}

server{
    listen 0.0.0.0:80;
    server_name chat.example.com;

    location / {
        proxy_pass http://127.0.0.1:6060;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

While this follows Nginx's official WebSocket documentation, it wasn't working as expected. The key issue was that the Upgrade header wasn't being properly passed through to the backend Node.js server.

After examining the traffic between Nginx and Node.js, I discovered that:

  • The direct connection included "Upgrade: websocket" header
  • Nginx wasn't properly forwarding this header
  • The Connection header was being set to "close" instead of "Upgrade"

The fixed configuration that properly handles WebSocket connections:

server{
    listen 0.0.0.0:80;
    server_name chat.example.com;

    location / {
        proxy_pass http://127.0.0.1:6060;
        proxy_http_version 1.1;
        proxy_set_header Upgrade "websocket";
        proxy_set_header Connection "Upgrade";
    }
}

Key improvements:

  • Explicitly set Upgrade header to "websocket"
  • Forced Connection header to "Upgrade"
  • Removed the problematic map block

The final production configuration includes additional headers to handle CORS and proper request forwarding:

server{
    listen 0.0.0.0:80;
    server_name chat.example.com;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://127.0.0.1:8081;
    }

    location /chat/ {
        proxy_pass http://127.0.0.1:6060;
        proxy_http_version 1.1;
        proxy_set_header Upgrade "websocket";
        proxy_set_header Connection "Upgrade";
    }
}

This separates regular HTTP traffic from WebSocket connections while ensuring proper header forwarding and CORS handling.

If you're still facing issues:

  1. Check Nginx error logs with tail -f /var/log/nginx/error_log
  2. Use ngrep to examine traffic between Nginx and backend
  3. Verify WebSocket implementation in your client code
  4. Check for corporate proxies or firewalls that might interfere