miguelgrinberg / Flask-SocketIO

Socket.IO integration for Flask applications.
MIT License
5.31k stars 888 forks source link

Namespace catch-all handling? #2061

Closed mooomooo closed 1 month ago

mooomooo commented 2 months ago

Describe the bug Namespace catch-all handlers don't seem to trigger?

To Reproduce Steps to reproduce the behavior:

  1. Configure socketio to accept catch-all namespaces: socketio = SocketIO(app, namespaces="*", ...)
  2. Set up a ns catch-all handler, e.g. @socketio.on('connect', namespace='*')
  3. Connect to server with a socketio client on any default or not-default namespace

Expected behavior Namespace catch-all handler gets called.

Logs

<ip> - - [04/May/2024 02:22:50] "GET /static/js/app.js HTTP/1.1" 304 -
_Kbzn1J5Dqad0cBkAAAG: Sending packet OPEN data {'sid': '_Kbzn1J5Dqad0cBkAAAG', 'upgrades': ['websocket'], 'pingTimeout': 20000, 'pingInterval': 25000} 
<ip> - - [04/May/2024 02:22:50] "GET /socket.io/?EIO=4&transport=polling&t=Oz1QKkO HTTP/1.1" 200 - 
_Kbzn1J5Dqad0cBkAAAG: Received packet MESSAGE data 0
_Kbzn1J5Dqad0cBkAAAG: Sending packet MESSAGE data 0{"sid":"z1ZH-ykQJMxhw58TAAAH"}

Additional context

mooomooo commented 2 months ago

Monkey patch:

import types
old_geh = socketio.server._get_event_handler
socketio.server.old_geh = socketio.server._get_event_handler
def new_geh(self, event, namespace, *args):
    sys.stderr.write(f"XXX e:{event} ns:{namespace}\n")
    h, a = self.old_geh(event, namespace, *args)
    sys.stderr.write(f">>> h:{h} a:{a}\n")
    return h, a
socketio.server._get_event_handler = types.MethodType(new_geh, socketio.server)

stderr output:

XXX e:connect ns:/
>>> h:<function on_connect at 0x7fb810513370> a:(...)
miguelgrinberg commented 2 months ago

The connect and disconnect handlers are not dispatched to catch-all handlers, only user-defined events are.

mooomooo commented 2 months ago

Hm is that intentional, or can they also be enabled? Without a connect catch-all, it doesn't seem like dynamic / arbitrary namespaces would be possible, i.e. all allowable namespaces would have to be explicitly assigned to a handler.


That said, something weird is still going on (which is what prompted my other post https://github.com/miguelgrinberg/python-socketio/issues/1334 in the first place): I've pared it down to this minimal server code. If a client connects on the default namespace then sends an "in" event , it gets lost with:

from flask import Flask, render_template, request 
from flask_socketio import SocketIO
import sys

app = Flask(__name__)
socketio = SocketIO(app, namespaces="*")

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

@socketio.on('connect') 
def on_connect(auth):
    sys.stderr.write(f"*** IN CONNECT w/ default ns\n")

@socketio.on('in', namespace='*')
def on_in_star(ns, data):
    sys.stderr.write(f"--- IN; catchall ns = {ns}, {data}\n")

if __name__ == '__main__':
    socketio.run(app, debug=True)

The ns catchall does not get triggered. But if I add a ns-specific handler:

@socketio.on('in', namespace='/')
def on_in(data):
    sys.stderr.write(f"/// IN; ns = '/', data = {data}\n")

that one does successfully get triggered.

For completeness, index.html:

<html>
  <head>
    <script src="/static/js/socket.io.min.js"></script>
  </head>

  <body>
    <script>
        const socket = io();

        socket.on('connect', function() {
            console.log("Connected to server, emitting 'hello' to 'in'");
            socket.emit('in', 'hello')
        });
    </script>
  </body>
</html>
miguelgrinberg commented 2 months ago

So your client is sending an event on a namespace it did not connect to? That does not work, the client must connect to the namespace(s) it wants to use.

mooomooo commented 2 months ago

(I'll split out the other issue into its own thread.)

miguelgrinberg commented 2 months ago

How is this different from the situation described in https://github.com/miguelgrinberg/python-socketio/issues/1334, which we agreed that is working correctly?

mooomooo commented 2 months ago

Yeah we agreed that it should work... but it doesn't. Again, here is a MWE, now all contained in a single file:

from flask import Flask
from flask_socketio import SocketIO

app = Flask(__name__)
socketio = SocketIO(app, namespaces="*", logger=True, engineio_logger=True)

@app.route('/')
def root():
    return '''<!DOCTYPE html>
<html>
  <head><script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script></head>
  <body><script>
        const socket = io();
        socket.on('connect', function() {
            socket.emit('in', 'hello')
        });
  </script></body>
</html>
'''

@socketio.on('connect')
def on_connect(auth):
    print(f"*** CONNECT; default ns\n")

@socketio.on('in', namespace='*')
def on_in_star(ns, data):
    print(f"--- IN; catchall ns = {ns}, {data}\n")

if __name__ == '__main__':
    socketio.run(app, debug=True)

If everything was working as intended, we would expect to see both print statements execute, i.e. if we see *** CONNECT; default ns then we should also see --- IN; catchall ns = /, hello. But we don't:

8S2llN_8QFMwcWC9AAAA: Sending packet OPEN data {'sid': '8S2llN_8QFMwcWC9AAAA', 'upgrades': ['websocket'], 'pingTimeout': 20000, 'pingInterval': 25000}
127.0.0.1 - - [08/May/2024 02:09:04] "GET /socket.io/?EIO=4&transport=polling&t=OzNTgOA HTTP/1.1" 200 -
8S2llN_8QFMwcWC9AAAA: Received packet MESSAGE data 0
*** CONNECT; default ns

8S2llN_8QFMwcWC9AAAA: Sending packet MESSAGE data 0{"sid":"qhmheUkfz-IN1VcHAAAB"}
127.0.0.1 - - [08/May/2024 02:09:04] "POST /socket.io/?EIO=4&transport=polling&t=OzNTgOR&sid=8S2llN_8QFMwcWC9AAAA HTTP/1.1" 200 -
8S2llN_8QFMwcWC9AAAA: Received request to upgrade to websocket
127.0.0.1 - - [08/May/2024 02:09:04] "GET /socket.io/?EIO=4&transport=polling&t=OzNTgOy&sid=8S2llN_8QFMwcWC9AAAA HTTP/1.1" 200 -
127.0.0.1 - - [08/May/2024 02:09:04] "GET /socket.io/?EIO=4&transport=polling&t=OzNTgOy.0&sid=8S2llN_8QFMwcWC9AAAA HTTP/1.1" 200 -
8S2llN_8QFMwcWC9AAAA: Upgrade to websocket successful
8S2llN_8QFMwcWC9AAAA: Received packet MESSAGE data 2["in","hello"]
received event "in" from qhmheUkfz-IN1VcHAAAB [/]

The received event should get dispatched to the in handler in the catch-all namespace... but it does not.

miguelgrinberg commented 1 month ago

Try the main branch of flask-socketio and let me know if that works better.

Note that your catch-all handler should be as follows:

@socketio.on('in', namespace='*')
def on_in_star(data):
    print(f"--- IN; catchall ns = {request.namespace}, {data}\n")

In Flask-SocketIO you do not need to add the namespace as an argument, because you always get it in request.namespace.

mooomooo commented 1 month ago

Worked! Looks like it's doing everything I expect now, thanks!

mooomooo commented 2 weeks ago

@miguelgrinberg Could you make a new release with this commit to save having to pip install from git? Thanks!

mooomooo commented 2 weeks ago

Also, there may need to be a similar fix to allow catch-all event handling? I don't think I can successfully do a

@socketio.on('*')

(but I haven't tried very much yet)

miguelgrinberg commented 2 weeks ago

I have never spent the time to add catch-all handlers in this package, and there is currently no testing beyond what I added for this fix. Before I can release this as a supported feature I need to do some more work and build tests.

mooomooo commented 2 weeks ago

Ah I see; thanks for your diligence!

x90slide commented 4 days ago

Please add catch all event handling. And on that subject, Please add dynamic event registering and unregistering at runtime.

miguelgrinberg commented 4 days ago

Please add dynamic event registering and unregistering at runtime.

Not planning to do this. You can implement this yourself for your application using a class-based namespace (or a catch-all event handler once I add them properly to this package) if this is important to you.