miguelgrinberg / python-socketio

Python Socket.IO server and client
MIT License
3.96k stars 587 forks source link

Server cannot emit to client - after getting stuck in websocket upgrade loop #1357

Closed PeterWaIIace closed 3 months ago

PeterWaIIace commented 3 months ago

You cannot emit anything Server is unable to emit anything after it goes into websocket upgrade wait.

To Reproduce Steps to reproduce the behavior:

  1. Create demo html:
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TEST</title>
    </head>
    <body>
    </body>


2. Create demo server:

import logging from flask import Flask, render_template from flask_socketio import SocketIO, emit

app = Flask(name) app.config['SECRET_KEY'] = 'secret!' socketio = SocketIO(app,logger=True)

logging.basicConfig(filename='app.log',level=logging.INFO) logger = logging.getLogger('socketio.server') handler = logging.FileHandler("app.log") handler.setLevel(logging.INFO) logger.addHandler(handler)

@app.route('/') def index(): return render_template('demo.html')

@socketio.on('connect') def handle_connect(): print('Client connected')

@socketio.on_error def error_handler(e): print(f'Error caught: {str(e)}')

@socketio.on_error_default def error_handler_def(e): print(f'Error caught: {str(e)}')

@socketio.on('msg') def test(message): print('msg -> rsp') emit("rsp","foo")

if name == 'main': socketio.run(app, debug=True, host='127.0.0.1', port=8000)


3. RUN - it will send message to server but server will never emit response

**Expected behavior**
Server should emit responses. 

**More description**

I am running my Flask and Socketio on `localhost` on port `8000` on `Ubuntu` with `gevent`. Tested on both `chrome` and `firefox`. I found that whenever `101 protocol upgrade` happens in webbrowsers logs - I no longer can emit anything back to client. Before that log I can send as many message as I want back and forth. 

I traced it back to python-socketio (in 5.11.3) Socket implementation. It seems that upgrade response is never obtained - meaning server waits forever, but that means all emitted messages are discarded. 

This is where server waits forever:
```Python
  if self.connected:
      # the socket was already connected, so this is an upgrade
      self.upgrading = True  # hold packet sends during the upgrade

      pkt = websocket_wait()
      decoded_pkt = packet.Packet(encoded_packet=pkt)
      if decoded_pkt.packet_type != packet.PING or \
              decoded_pkt.data != 'probe':
          self.server.logger.info(
              '%s: Failed websocket upgrade, no PING packet', self.sid)
          self.upgrading = False
          return []
      ws.send(packet.Packet(packet.PONG, data='probe').encode())
      self.queue.put(packet.Packet(packet.NOOP))  # end poll

      **pkt = websocket_wait()** << IT NEVER EXITS THIS FUNCTION HERE
      decoded_pkt = packet.Packet(encoded_packet=pkt)
      if decoded_pkt.packet_type != packet.UPGRADE:
          self.upgraded = False
          self.server.logger.info(
              ('%s: Failed websocket upgrade, expected UPGRADE packet, '
               'received %s instead.'),
              self.sid, pkt)
          self.upgrading = False
          return []
      self.upgraded = True
      self.upgrading = False
  else:
      self.connected = True
        self.upgraded = True

This is where messages are discarded:

def handle_get_request(self, environ, start_response):
    """Handle a long-polling GET request from the client."""
    connections = [
        s.strip()
        for s in environ.get('HTTP_CONNECTION', '').lower().split(',')]
    transport = environ.get('HTTP_UPGRADE', '').lower()
    if 'upgrade' in connections and transport in self.upgrade_protocols:
        self.server.logger.info('%s: Received request to upgrade to %s',
                                self.sid, transport)
        return getattr(self, '_upgrade_' + transport)(environ,
                                                      start_response)
    # if self.upgrading or self.upgraded:
        # we are upgrading to WebSocket, do not return any more packets
        # through the polling endpoint
        # print("NOOPING")
        # return [packet.Packet(packet.NOOP)]
    try:
        packets = self.poll()
    except exceptions.QueueEmpty:
        exc = sys.exc_info()
        self.close(wait=False)
        raise exc[1].with_traceback(exc[2])
    return packets

By commenting ou return [packet.Packet(packet.NOOP)] server can emit again.

Work around Fallback to 5.11.2

miguelgrinberg commented 3 months ago

What version of python-engineio are you using? What I find strange is that none of the changes in python-socketio 5.11.3 are related to the WebSocket transport, in fact this package doesn't really work at that level at all, since this is what the python-engineio package does.

PeterWaIIace commented 3 months ago

Hey, thanks for quick response.

I am currently using python-engineio Version: 4.9.1.

So after your comment, I went back and tested server again with python-socketio 5.11.3 and after reinstallation it works fine?! Sorry, I felt like I spent eternity trying to pinpoint issue, and when finally it worked again with 5.11.2 was sure it was that. But maybe pip install did change some other package too.

So well, that one was false-positive. Sorry for bothering you.