1002 Protocol Error — Invalid Frame from Proxy
الأعراض
- WebSocket connection closes with code 1002 shortly after the upgrade handshake
- Close reason references 'invalid frame', 'RSV bits set without negotiated extension', or 'unexpected opcode'
- The issue occurs only when the connection passes through an HTTP proxy or corporate firewall
- Direct connection (bypassing the proxy) works correctly
- Wireshark shows the proxy modifying WebSocket frames mid-stream or injecting HTTP responses
- Close reason references 'invalid frame', 'RSV bits set without negotiated extension', or 'unexpected opcode'
- The issue occurs only when the connection passes through an HTTP proxy or corporate firewall
- Direct connection (bypassing the proxy) works correctly
- Wireshark shows the proxy modifying WebSocket frames mid-stream or injecting HTTP responses
الأسباب الجذرية
- HTTP proxy or firewall treating WebSocket frames as HTTP traffic and modifying the payload
- Proxy applying permessage-deflate compression to frames without the extension being negotiated in the opening handshake
- Client or server sending continuation frames (opcode 0x0) without an initial data frame
- Server sending masked frames to the client — RFC 6455 requires only client-to-server masking
- Binary frame received when the handler only accepts text (opcode 0x1), or vice versa
التشخيص
**Step 1 — Determine if a proxy is the cause**
```bash
# Test connection directly to the application server (bypassing the proxy):
wscat -c ws://backend-server:8001/ws
# If direct works but through-proxy fails → proxy is mangling frames
# Check if a proxy is in the path:
curl -v https://api.example.com/ 2>&1 | grep -i 'via\|x-forwarded\|proxy'
```
**Step 2 — Capture frames with Wireshark**
```bash
# Capture on the server-side interface:
sudo tcpdump -i eth0 -n 'port 8001' -w /tmp/ws.pcap
# Open in Wireshark → filter: websocket
# Look for:
# - FIN=0 frames without a continuation FIN=1 to follow
# - Non-zero RSV bits when no extension was negotiated
# - MASK bit set in server-to-client direction (protocol violation)
```
**Step 3 — Check the negotiated extensions in the handshake**
```bash
# Inspect the upgrade response headers:
curl -v -i \
-H 'Upgrade: websocket' \
-H 'Connection: Upgrade' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
-H 'Sec-WebSocket-Version: 13' \
http://api.example.com/ws 2>&1 | grep -i 'Sec-WebSocket-Extensions'
# If server returns 'Sec-WebSocket-Extensions: permessage-deflate'
# but a proxy strips this header → RSV1 bit set without negotiation → 1002
```
**Step 4 — Test frame types**
```javascript
// Test whether the server rejects binary frames:
ws.onopen = () => {
// Send binary:
ws.send(new Uint8Array([1, 2, 3]));
// Send text:
ws.send(JSON.stringify({ type: 'ping' }));
};
ws.onerror = (e) => console.error('Error after binary send?', e);
// If error appears after binary send → server requires text frames only
```
**Step 5 — Check Nginx WebSocket upgrade configuration**
```bash
# Verify Nginx is properly upgrading the connection:
grep -A10 'location.*ws\|proxy_set_header Upgrade' /etc/nginx/sites-available/yoursite
# Must include:
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection 'upgrade';
# proxy_http_version 1.1;
```
```bash
# Test connection directly to the application server (bypassing the proxy):
wscat -c ws://backend-server:8001/ws
# If direct works but through-proxy fails → proxy is mangling frames
# Check if a proxy is in the path:
curl -v https://api.example.com/ 2>&1 | grep -i 'via\|x-forwarded\|proxy'
```
**Step 2 — Capture frames with Wireshark**
```bash
# Capture on the server-side interface:
sudo tcpdump -i eth0 -n 'port 8001' -w /tmp/ws.pcap
# Open in Wireshark → filter: websocket
# Look for:
# - FIN=0 frames without a continuation FIN=1 to follow
# - Non-zero RSV bits when no extension was negotiated
# - MASK bit set in server-to-client direction (protocol violation)
```
**Step 3 — Check the negotiated extensions in the handshake**
```bash
# Inspect the upgrade response headers:
curl -v -i \
-H 'Upgrade: websocket' \
-H 'Connection: Upgrade' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
-H 'Sec-WebSocket-Version: 13' \
http://api.example.com/ws 2>&1 | grep -i 'Sec-WebSocket-Extensions'
# If server returns 'Sec-WebSocket-Extensions: permessage-deflate'
# but a proxy strips this header → RSV1 bit set without negotiation → 1002
```
**Step 4 — Test frame types**
```javascript
// Test whether the server rejects binary frames:
ws.onopen = () => {
// Send binary:
ws.send(new Uint8Array([1, 2, 3]));
// Send text:
ws.send(JSON.stringify({ type: 'ping' }));
};
ws.onerror = (e) => console.error('Error after binary send?', e);
// If error appears after binary send → server requires text frames only
```
**Step 5 — Check Nginx WebSocket upgrade configuration**
```bash
# Verify Nginx is properly upgrading the connection:
grep -A10 'location.*ws\|proxy_set_header Upgrade' /etc/nginx/sites-available/yoursite
# Must include:
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection 'upgrade';
# proxy_http_version 1.1;
```
الحل
**Fix 1 — Configure Nginx to properly pass WebSocket frames**
```nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location /ws/ {
proxy_pass http://app_server;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 3600s; # long timeout for WS
proxy_send_timeout 3600s;
}
}
```
**Fix 2 — Disable permessage-deflate if a proxy strips the extension header**
```python
# Python websockets — disable compression:
async with websockets.serve(
handler, 'localhost', 8765,
compression=None # disable permessage-deflate
) as server:
await server.serve_forever()
```
```javascript
// Node.js ws — disable per-message deflate:
const wss = new WebSocket.Server({
perMessageDeflate: false
});
```
**Fix 3 — Bypass proxy for WebSocket traffic**
```bash
# Route WebSocket traffic on a separate port that bypasses the offending proxy:
# Example: serve HTTP on 443, WebSocket on 8443
# Client:
const ws = new WebSocket('wss://api.example.com:8443/ws');
# Or use CONNECT tunneling through the proxy if supported:
wscat -c wss://api.example.com/ws --proxy http://corporate-proxy:3128
```
**Fix 4 — Fix server-side frame type enforcement**
```python
# Accept both text and binary frames:
class FlexibleConsumer(AsyncWebsocketConsumer):
async def receive(self, text_data=None, bytes_data=None):
if text_data is not None:
await self.handle_text(text_data)
elif bytes_data is not None:
await self.handle_binary(bytes_data)
```
```nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location /ws/ {
proxy_pass http://app_server;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 3600s; # long timeout for WS
proxy_send_timeout 3600s;
}
}
```
**Fix 2 — Disable permessage-deflate if a proxy strips the extension header**
```python
# Python websockets — disable compression:
async with websockets.serve(
handler, 'localhost', 8765,
compression=None # disable permessage-deflate
) as server:
await server.serve_forever()
```
```javascript
// Node.js ws — disable per-message deflate:
const wss = new WebSocket.Server({
perMessageDeflate: false
});
```
**Fix 3 — Bypass proxy for WebSocket traffic**
```bash
# Route WebSocket traffic on a separate port that bypasses the offending proxy:
# Example: serve HTTP on 443, WebSocket on 8443
# Client:
const ws = new WebSocket('wss://api.example.com:8443/ws');
# Or use CONNECT tunneling through the proxy if supported:
wscat -c wss://api.example.com/ws --proxy http://corporate-proxy:3128
```
**Fix 4 — Fix server-side frame type enforcement**
```python
# Accept both text and binary frames:
class FlexibleConsumer(AsyncWebsocketConsumer):
async def receive(self, text_data=None, bytes_data=None):
if text_data is not None:
await self.handle_text(text_data)
elif bytes_data is not None:
await self.handle_binary(bytes_data)
```
الوقاية
- Always test WebSocket connections through the full proxy/CDN stack in staging, not just direct
- Disable permessage-deflate compression by default when corporate proxies are in the traffic path
- Use Wireshark or Chrome DevTools WebSocket inspector to capture frame-level traffic during testing
- Document the Nginx WebSocket proxy configuration in your infrastructure templates
- Run a WebSocket protocol conformance test suite (e.g., Autobahn Testsuite) against your server before deploying major changes
- Disable permessage-deflate compression by default when corporate proxies are in the traffic path
- Use Wireshark or Chrome DevTools WebSocket inspector to capture frame-level traffic during testing
- Document the Nginx WebSocket proxy configuration in your infrastructure templates
- Run a WebSocket protocol conformance test suite (e.g., Autobahn Testsuite) against your server before deploying major changes