We all know we could use curl anytime with HTTP requests but what about WebSockets? I was debugging a WebSocket the other day which was basically a Docker service sitting behind an Nginx reverse proxy so I figured I’d write it down.

But, first of all, a bit of basics.

http vs websockets

HTTP is request and response. Stateless. You ask for something, the server responds, and that’s it.

WebSockets are stateful. They create a persistent, two-way connection between your browser and the server on. Think of it like UDP but running on top of TCP and as part of the application layer lol.

The tricky part? When you’re using Nginx as a reverse proxy, the initial WebSockets connection actually starts as a regular HTTP request that gets “upgraded” to a WebSockets connection. This is where things can get confusing during debugging.

curl

Before we can test anything with curl, we need a WebSockets key. This is basically a security handshake that proves both the client and server actually understand the WebSockets protocol.

So, generate one first:

openssl rand -base64 16

or even:

head -c 16 /dev/urandom | base64

Either way, you’ll get something like: XrBxDCiSL/2Bxptca15PEw==

Then:

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: WebSocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: <your_generated_base64_encoded_websockets_key_here>" \
  http://localhost/ws

Quick notes:

  • Don’t forget to replace http://localhost/ws with your actual WebSockets endpoint.
  • If you get the “101 Switching Protocols” response, WebSocket is working.
  • The -i flag shows response headers, and -N disables buffering (important for real-time data).

what the response codes actually mean

When debugging, the HTTP status code before the upgrade tells you a lot:

  • 101 Switching Protocols — success, the connection upgraded to WebSocket
  • 400 Bad Request — your headers are wrong or missing (double check Upgrade, Connection, and Sec-WebSocket-Key)
  • 426 Upgrade Required — the server requires WebSocket but you’re not sending the right headers
  • 502 Bad Gateway — Nginx can’t reach your backend service at all, WebSocket isn’t even the problem
  • 504 Gateway Timeout — Nginx reached the backend but the connection timed out (see timeout section below)

testing with authentication

If your WebSocket endpoint requires auth, add the header the same way:

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: WebSocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: XrBxDCiSL/2Bxptca15PEw==" \
  -H "Authorization: Bearer your_token_here" \
  http://localhost/ws

Same goes for cookies if your app uses session-based auth:

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: WebSocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: XrBxDCiSL/2Bxptca15PEw==" \
  -H "Cookie: session=your_session_cookie" \
  http://localhost/ws

testing wss:// (secure websockets)

If your endpoint is behind TLS, swap http:// for https:// — curl handles the TLS handshake automatically:

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: WebSocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: XrBxDCiSL/2Bxptca15PEw==" \
  https://yourdomain.com/ws

Add -k to skip certificate verification if you’re testing with a self-signed cert locally.

nginx

You’ll need to modify two files to get WebSockets Nginx proxy working properly.

First, add this to your /etc/nginx/nginx.conf:

http {
    #... some config ...

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

    #... rest of config ...
}

Then in your site config e.g. /etc/nginx/conf.d/default.conf:

server {
    # ... some config ...

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

Quick notes:

  • Don’t forget to update proxy_pass http://backend to point to wherever your WebSocket service is actually running.
  • HTTP/1.1 is important here. Websockets don’t really “use” HTTP/1.1 ongoing. They just borrow it for the handshake, then switch to their own protocol.
  • Technically you could run WebSockets on HTTP/2 however its not really popular choice. Doable, but not practically viable at all.

the timeout problem

This one catches everyone. Nginx’s default proxy_read_timeout is 60 seconds. WebSocket connections are supposed to stay open indefinitely, so after 60 seconds of inactivity Nginx will close the connection with a 504.

Fix it by bumping the timeouts in your location block:

location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

3600s is one hour. Set it based on how long your connections actually need to stay open. Most apps also implement a ping/pong heartbeat on the client side to keep the connection alive — that’s the proper solution, but bumping the timeout is the quick fix.

bottom line

And there’s that. With these Nginx configs and the curl command, you’ve got everything you need to test and verify WebSocket connections… to a certain extent.

Still monitoring your infra manually? Maybe, just maybe give justanotheruptime.com a look ;)