WebSocket 400 Error Debugging Guide
Ops runbook for diagnosing and fixing WebSocket handshake 400 errors in production — covers auth failures, Nginx proxy config, and token issues.
Problem
Getting Handshake exception, expected status code 101 but was 400 when connecting to WebSocket in production.
What the Logs Will Show
Client-Side Logs (Desktop App)
When you try to connect, you should see:
🔌 [WebSocket] Connecting to wss://api.estebanruano.com:443/api/v1/timers/notifications
🔌 [WebSocket] Host: api.estebanruano.com, Port: 443, Path: /api/v1/timers/notifications
🔌 [WebSocket] Token present: true, Token length: XXX
🔌 [WebSocket] Token preview: Bearer eyJhbGciOiJIUzI1...
🔌 [WebSocket] Request headers:
- Authorization: Bearer eyJhbGciOiJIUzI1...
- Connection: Keep-Alive
- ...If connection fails:
🚫 [WebSocket] Failed to connect to WebSocket: Handshake exception...
🚫 [WebSocket] Exception type: WebSocketExceptionServer-Side Logs
Check your server logs for:
========== WebSocket Connection Attempt ==========
URI: /api/v1/timers/notifications
Method: GET
Path: /api/v1/timers/notifications
Headers (X):
- Authorization: Bearer ...
- Upgrade: websocket
- Connection: Upgrade
...If authentication fails:
========== WebSocket Connection REJECTED ==========
Reason: No authenticated user found
Authorization header present: true/falseCommon Causes of 400 Error
1. Authentication Failure (Most Common)
Symptoms:
- Server logs show “No authenticated user found”
- Authorization header is present but invalid/expired
Solutions:
- Check if token is valid and not expired
- Verify token format:
Bearer <token> - Check JWT secret matches between client and server
- Ensure token is being sent in Authorization header
2. Missing Headers
Symptoms:
- Server logs show missing
UpgradeorConnectionheaders - Nginx might be stripping headers
Solutions:
- Check nginx config has proper WebSocket headers:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Authorization $http_authorization;
3. Nginx Rejecting Request
Symptoms:
- No server logs appear (request never reaches server)
- 400 error happens before authentication
Solutions:
- Check nginx error logs:
docker logs <nginx-container> - Verify nginx WebSocket location block is correct
- Check if nginx is returning 400 due to method restrictions
4. Wrong Path or Port
Symptoms:
- Client connecting to wrong URL
- Port mismatch (8080 vs 443)
Solutions:
- Verify
SOCKETS_HOST,SOCKETS_PORT,SOCKETS_PATHconstants - For production, use port 443 with
wss:// - For dev, use port 8080 with
ws://(if nginx allows)
Diagnosis
Follow this decision tree top-to-bottom; each branch identifies the exact fix needed.
Debugging Steps
Step 1: Check Client Logs
- Run the desktop app
- Try to connect to WebSocket
- Look for the
🔌 [WebSocket]log messages - Verify:
- URL is correct
- Token is present
- Headers are being sent
Step 2: Check Server Logs
- SSH into your server or check Docker logs:
docker logs <server-container> | grep -i websocket docker logs <server-container> | grep -i "WebSocket Connection" - Look for:
- “WebSocket Connection Attempt” - request reached server
- “WebSocket Connection REJECTED” - authentication failed
- “WebSocket Connection ACCEPTED” - connection successful
Step 3: Check Nginx Logs
- Check nginx access logs:
docker logs <nginx-container> | grep timers/notifications - Check nginx error logs:
docker logs <nginx-container> 2>&1 | grep -i error
Step 4: Test Direct Connection (Bypass Nginx)
If possible, test connecting directly to the backend server (port 8080) to see if nginx is the issue.
Step 5: Verify Configuration
-
Client:
- Check
DesktopConstants.kt- verifySOCKETS_HOST_PROD,SOCKETS_PORT_PROD - Ensure
VARIANT = PROD_VARIANTfor production - Verify certificate is in
composeApp/src/desktopMain/resources/certs/cloudflare-cert.pem
- Check
-
Server:
- Check JWT secret matches
- Verify authentication is configured correctly
- Check WebSocket route is properly set up
-
Nginx:
- Verify WebSocket location block exists
- Check headers are being forwarded
- Ensure no method restrictions blocking WebSocket upgrade
Quick Test Commands
Test WebSocket with curl (from server)
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
https://api.estebanruano.com/api/v1/timers/notificationsCheck if request reaches server
# Watch server logs in real-time
docker logs -f <server-container> | grep -i websocketExpected Behavior
Successful Connection
Client:
✅ WebSocket connectedServer:
========== WebSocket Connection ACCEPTED ==========
User ID: 123Failed Connection (Authentication)
Client:
🚫 [WebSocket] Failed to connect to WebSocket: Handshake exception...Server:
========== WebSocket Connection REJECTED ==========
Reason: No authenticated user foundNext Steps
- Run the app and collect logs from both client and server
- Compare the logs with this guide
- Identify which step is failing
- Apply the appropriate solution