Closed cheahjs closed 3 weeks ago
Code flow (for the async server):
Existing session GET request:
Socket checks for Upgrade
and Connection: upgrade
headers to perform an upgrade, but falls back to assuming it's a long-polling request regardless of the transport
query parameter.
This is handled correctly in the case of attempting a direct websocket connection without upgrading an existing session (`transport == upgrade_header == 'websocket' check):
@cheahjs if your server is not going to accept WebSocket upgrade requests then I suggest you add the allow_upgrades=False
option to avoid the unnecessary WebSocket connection failure.
Describe the bug When configured to upgrade from polling to websockets, but the server is incapable of a websocket connection, the upgrade request (eg
GET /socket.io/?EIO=4&transport=websocket&sid=OOOgsMLnIxkuk9GMAABb
) is treated as a long-polling request, which consumes server-emitted events in the queue, but never reaches clients because clients such as browsers would drop the response body since it's not valid websocket.With the javascript client, this can cause race conditions where the websocket upgrade request consumes the
CONNECT
message, and the client breaks as a result.To Reproduce
Here is an example for reproducing this behaviour:
server.py
```python import socketio import uvicorn sio = socketio.AsyncServer( async_mode="asgi", logger=True, engineio_logger=True, always_connect=True, cors_allowed_origins="*", ) app = socketio.ASGIApp( sio, static_files={ "/": "index.html", }, ) @sio.on async def connect(sid, environ): print(f"Connected: {sid}") uvicorn.run(app, port=8000) ```index.html - Javascript client to show that it races and occasionally never connects.
```htmlSocket.IO test
Check the console logs
```nginx.conf for replicating a reverse proxy that is websocket-incapable (start with `nginx -c $(pwd)/nginx.conf -g 'daemon off;'`)
``` events { worker_connections 1024; } error_log /dev/stderr; http { server { access_log /dev/stdout; listen 9000; server_name localhost; location / { proxy_pass http://localhost:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } } ```upgrade.sh script to show that the websocket upgrade is treated as long-polling and consumes events
```bash #!/usr/bin/env bash echo "Starting handshake" # Extract "sid":"..." from the response sid=$(curl --silent 'http://localhost:9000/socket.io/?EIO=4&transport=polling' | grep -o '"sid":"[^"]*"' | cut -d'"' -f4) echo "sid: $sid" echo "" echo "Sending CONNECT" curl -X POST 'http://localhost:9000/socket.io/?EIO=4&transport=polling&sid='"$sid" -d '40' & echo "" echo "Attempting to consume any events via polling prior to upgrade - racing" # Run in the background to race with the upgrade curl --silent 'http://localhost:9000/socket.io/?EIO=4&transport=polling&sid='"$sid" & echo "" echo "Attempting to upgrade" echo "" # Use raw netcat to see responses ( cat <Expected behavior A websocket upgrade request without the correct
Upgrade
orConnection
headers is treated as an invalid upgrade request instead of a long-polling request.Logs
Logs showing websocket request consuming events:
And logs showing the other side of the race, where long-polling gets the CONNECT request: