Intermediate 15 min WebSocket 1006

WebSocket 1006 Abnormal Closure

Belirtiler

- `onclose` event fires with `event.code === 1006` and `event.wasClean === false`
- No close frame in browser DevTools Network tab — connection entry shows no response code
- Connection drops after exactly 30, 60, or 120 seconds (proxy idle timeout)
- Server process exits or crashes with no graceful shutdown, clients get 1006 immediately
- wscat shows `error: socket hang up` or `disconnected` without a code

Kök Nedenler

  • Reverse proxy (Nginx, AWS ALB, Cloudflare) closes idle WebSocket connections after its timeout period
  • Server process crashed or was OOM-killed without sending a close frame to clients
  • Network interruption (Wi-Fi drop, mobile handoff) abruptly terminates the TCP connection
  • Missing ping/pong heartbeat — neither client nor server sends keep-alive frames, proxy drops the idle connection
  • Cloudflare 100-second WebSocket idle timeout not overridden by ping/pong activity

Tanı

**Step 1 — Capture the close event in the browser**

```javascript
const ws = new WebSocket('wss://example.com/ws');

ws.onclose = (event) => {
console.log('Close code:', event.code); // 1006 = no close frame
console.log('Reason:', event.reason); // usually empty for 1006
console.log('Was clean:', event.wasClean); // false for 1006
console.log('Timestamp:', new Date().toISOString());
};

ws.onerror = (err) => console.error('WS Error:', err);
```

**Step 2 — Test with wscat from the command line**

```bash
# Install: npm install -g wscat
wscat -c wss://example.com/ws
# Note the exact time of disconnection
# If it drops at T+60s, Nginx proxy_read_timeout = 60s
```

**Step 3 — Check proxy timeout settings**

```bash
# Nginx
grep -r 'proxy_read_timeout\|proxy_send_timeout' /etc/nginx/

# AWS ALB: check idle timeout in the console
aws elbv2 describe-load-balancer-attributes \
--load-balancer-arn arn:aws:elasticloadbalancing:... \
| grep idle_timeout
```

**Step 4 — Inspect server-side WebSocket handler for crashes**

```bash
# Check server logs for unhandled exceptions
journalctl -u my-websocket-service -n 100 --no-pager | grep -E 'ERROR|Exception|crash|OOM'

# Check OOM kills
dmesg | grep -i 'oom\|killed process'
```

**Step 5 — Measure ping/pong round trips**

```bash
# wscat supports ping
wscat -c wss://example.com/ws --no-check
# Type: /ping
# Server should respond with a pong frame
# In Chrome DevTools: Network → WS connection → Frames tab → filter by ping/pong
```

Çözüm

**Fix 1 — Increase Nginx proxy timeout for WebSocket**

```nginx
# /etc/nginx/sites-available/mysite
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # 1 hour
proxy_send_timeout 3600s;
proxy_connect_timeout 75s;
}
```

**Fix 2 — Add server-side ping/pong heartbeat**

```python
# Django Channels example
import asyncio
from channels.generic.websocket import AsyncWebsocketConsumer

class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
self._ping_task = asyncio.create_task(self._heartbeat())

async def disconnect(self, close_code):
self._ping_task.cancel()

async def _heartbeat(self):
while True:
await asyncio.sleep(30) # ping every 30s
await self.send(text_data='{"type": "ping"}')
```

**Fix 3 — Add client-side reconnection with exponential backoff**

```javascript
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.delay = 1000;
this.maxDelay = 30000;
this.connect();
}

connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => { this.delay = 1000; };
this.ws.onclose = (e) => {
if (e.code === 1006 || !e.wasClean) {
console.log(`Reconnecting in ${this.delay}ms...`);
setTimeout(() => this.connect(), this.delay);
this.delay = Math.min(this.delay * 2, this.maxDelay);
}
};
}
}
```

**Fix 4 — Add ping/pong to keep Cloudflare connection alive**

```javascript
// Cloudflare drops idle WS after 100s; ping every 50s
const ws = new WebSocket('wss://example.com/ws');
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 50_000);
ws.onclose = () => clearInterval(pingInterval);
```

Önleme

- Always implement a ping/pong heartbeat at 30–50 second intervals on both client and server
- Set proxy_read_timeout in Nginx to match or exceed your application's expected idle window
- Implement client-side automatic reconnection with exponential backoff for all 1006 closures
- Add server process monitoring (systemd RestartSec, Kubernetes liveness probe) to restart crashed servers
- Log all WebSocket close events server-side with code, reason, and connection duration for post-mortem analysis

İlgili Durum Kodları

İlgili Terimler