Flask-SocketIO and the User Session

Posted by
on under

The way user sessions are handled in my Flask-SocketIO extension has always been a pain point for me. I tried to make sessions work almost the same as they work on regular Flask routes, but the "almost" part is what makes it confusing for most people.

In this short article and its companion video, I will try to explain why this is not trivial, and also will go over some improvements I just released that I hope will improve the use cases on which users seem to always trip.

Default User Session Handling

The way user sessions are handled by default is by forking the Flask user session at the time the client connects to the server over Socket.IO. What does it mean to "fork" the session? It means that the contents of the Flask user session are copied over to a brand new session, specifically created for the Socket.IO connection. This session is different than the Flask session, it is actually handled by the Flask-SocketIO extension.

In practice, this handling of user sessions means that Socket.IO event handlers are going to see anything that was in the Flask user session at the time of connection. But any changes that are made to the Flask session through HTTP routes after the Socket.IO connection took place will not be accessible from Socket.IO handlers. Likewise, any changes made to the session from Socket.IO handlers will not be accessible through regular Flask routes. There are basically two user sessions, one for HTTP and one for Socket.IO.

To summarize:

  • The Flask session is copied to the Socket.IO session at the time of the Socket.IO connection.
  • Changes made to the session in Flask routes after the Socket.IO connection was made will not be accessible on the Socket.IO session.
  • Changes made to the session in Socket.IO event handlers will not be accessible on the Flask user session.

You may wonder why such a convoluted way to handle sessions. The reason lies in the fact that the server is unable to send cookies to the client through a WebSocket connection. If a Socket.IO handler makes a change to the user session, a new version of the session cookie would need to be sent to the client, and there is no standard way to do that.

Flask-Socketio's New Managed Sessions Setting

Starting with the 2.9.0 release of Flask-SocketIO, there is a new setting that controls how sessions are managed by the extension, with the optional manage_session argument given to the SocketIO class. The default value is manage_session=True, which means that Flask-SocketIO will manage its own user sessions, as described in the previous section.

Passing manage_session=False disables the extension's handling of user sessions, and instead, the Flask session handling is used. This has no practical use when working with the regular Flask user sessions based on cookies, because as explained above, cookies cannot be sent to the client on a WebSocket connection, but there are a few Flask extensions that implement server-side sessions, which for most usages, bypass the problem of having to send cookies to a client connected over WebSocket.

For example, the Flask-Session extension supports sessions with server-side storage on Redis, Memcached, MongoDB, SQLAlchemy databases or regular disk files. Updating any of these sessions is possible in a Socket.IO event handler, as long as the client already has the session id, which should be true when the session is first accessed from a regular HTTP route. As an additional limitation, the session cannot be discarded, as that will cause a new session to be created, and that will change the session id.

Using Server-Side Sessions

To use server side sessions with Flask-Session, you just need to initialize the extension and decide what storage you want to use for your sessions. The easiest configuration is to use disk files:

from flask_socketio import SocketIO
from flask_session import Session

app.config['SESSION_TYPE'] = 'filesystem'
Session(app)
socketio = SocketIO(app, manage_session=False)

With the above configuration, the server will create a subdirectory called flask_session in the current directory and write user sessions for all clients in it. These files will be written to by Flask or by Flask-SocketIO whenever changes to the session are made.

If you set manage_session=True instead, the user sessions will continue to be forked as described above, regardless of what type of session you use.

There is a complete example Flask-SocketIO application that uses this type of sessions in the official repository, called sessions.py. In the video above I demonstrate how this application works using all the different session modes available in the 2.9.0 release.

Flask-Login Support

I frequently receive questions regarding how to use Flask-Login with Flask-SocketIO. The good news is that the current_user context variable is fed from the user session, so that means that you can reference current_user in your Socket.IO event handlers without any problems. And if you set manage_session=False in combination with server-side sessions, you can also use login_user() and logout_user() from Socket.IO event handlers, and the changes to the user session are going to also be seen from Flask routes.

The login_required decorator is more tricky. This decorator was designed to work with Flask routes, so it cannot be used on Socket.IO event handler functions. The Flask-SocketIO documentation includes a custom decorator that has similar functionality as Flask-Login's login_required, but is designed to work with Socket.IO event handlers. For your convenience, here is the decorator source code:

import functools
from flask_login import current_user
from flask_socketio import disconnect

def authenticated_only(f):
    @functools.wraps(f)
    def wrapped(*args, **kwargs):
        if not current_user.is_authenticated:
            disconnect()
        else:
            return f(*args, **kwargs)
    return wrapped

You can use this decorator as follows:

@socketio.on('my event')
@authenticated_only
def handle_my_custom_event(data):
    emit('my response', {'message': '{0} has joined'.format(current_user.name)},
        broadcast=True)

Conclusion

I hope the new server-side session handling in Flask-SocketIO 2.9.0 helps alleviate the problems I see users having. If you have any suggestions to make more improvements, or you have experienced problems with user sessions not addressed with these changes, definitely let me know in the comments below, or with an issue on the GitHub repository.

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

24 comments
  • #1 andy said

    Hi,
    this is a very interesting post.
    Can this solution work with jwt?

    Regards

  • #2 Miguel Grinberg said

    @andy: You can use JWTs for authentication, but that is unrelated to the topic of this article, which is how to share data between HTTP and Socket.IO handlers through the user session.

  • #3 christian said

    Hi Miguel,
    many thanks for creating and maintaining flask-socketio! In a 'split-session' set up (socketio session is forked from flask (client-side) session) is it possible to use the flask-socketio testclient and access the socketio session? Something similar to the test_request_context() method of the flask testclient? Ideally I'd like to modify the socketio session during testing.

  • #4 Miguel Grinberg said

    @christian: the session is maintained in a dictionary, so you could figure out how to make changes if you look at the source code. Instead, what I would recommend is that you add a new event in your test's setUp() function, and you set the session values that you need by triggering that event through the test client.

  • #5 rilwan said

    I have flask web server and js on client side.
    Client side-
    socket.on('connect', function () {
    socket.emit('my_event', {data: 'I\'m connected!'});
    });
    Server side
    @socketio.on('connect', namespace='/test')
    def test_connect():
    # What code here to print the emited data on server side.

    How to recive json at server side emited from clien. All examples are emit on both sides. On cleint side I can recieve data emited from server by below code. I want similar code in sever side(in python flask)
    socket.on('my_response', function (msg) {
    writeLog('log', 'Received #' + msg.count + ': ' + msg.data, 'warning')
    });
    Please hlep. Much appreciated.

  • #6 Miguel Grinberg said

    @rilwan: Use the following on the server:

    @socketio.on('my_event', data):
    # put your handling code here

  • #7 Rohit Thapliyal said

    Hi Miguel,
    Thanks for this wonderful socketio Flask extension.
    I have an issue. I have made a chat-bot using the extension. Now, I have a function which listens at the server end (Python) and emits an appropriate response. Now, there is a case when a global variable 'flag' is changed from 0 to 1 in this function. Now, when a user is reaching this condition(changing flag to 1), the code will work for flag = 1 case. But, for every user, the value of flag is getting changed (since flag is a global variable). The different socket connections are sharing the same instance of the server-end Python script.

    I used sessions the way you have directed to, but still, if flag is getting changed to 1, it gets altered for other socket connections too.

    Here is the pseudo code:

    app = Flask(name)
    app.config['SESSION_TYPE'] = 'filesystem'
    Session(app)

    socketio = SocketIO(app, manage_session=False)

    flag = 0

    @socketio.on('message')
    def chat(message):
    emit('ok', response[message])
    ....

    if name == 'main':
    socketio.run(app)

    Please, point out where I went wrong.

  • #8 Miguel Grinberg said

    @Rohit: you have to put your flag in the user session. For example: session['flag'] = 1.

  • #9 joaquin berenguer berenguer said

    Miguel,
    My intention is to use only the your Session in the SocketIO environment, but you said you copied the previous session into a new session. In my case the previous session does not exist, any problem?

  • #10 Miguel Grinberg said

    @joaquin: that is fine. If you had no previous session, then an empty session will be initialized for each Socket.IO client.

  • #11 Greg said

    Thank you for this tutorial. I have 2 questions:
    1- Would you recommend using flask_session, given that it's been a while that the project is inactive (the last update on Github is from couple of years ago)? Are there any alternatives that you would recommend.
    2- Using flask_session, I want to use sqlalchemy configuration instead of filesystem, however I get the error that the sessions table (the default name) is not created. The documentation doesn't mention any db creation and one would assume that the table should be created automatically, could you let me know if there is any automatic way of creating the table or should I just create the table manually?

  • #12 Miguel Grinberg said

    @Greg: I have used Flask-Session and never had problems with it. An alternative is Flask-KVSession, and I'm sure there are a few more on GitHub. I think the current version has a bug, it should be calling db.create_all() to trigger the creation of the table, but that line was commented out.

  • #13 Tailane Brito said

    Hello Miguel, Thanks so much for this material!
    I'm having a problem when trying to import the flask_login package. Do you have an alternative way to setup it manually? I'm trying to call the package using " from login_manager import * " after adding it to my project folder but I still get the error:

    File "/Users/tsaibrito/TailaneBrito/project2/login_manager.py", line 15, in <module>
    from ._compat import text_type
    ImportError: attempted relative import with no known parent package

    Do you have any advice for me? thanks so much!

  • #14 Miguel Grinberg said

    Flask-Login is composed of several files .If you want to do a manual installation, then create a flask_login subdirectory and copy all the modules inside it.

  • #15 Dilip Yadav said

    Hi,

    I came across your blog and found it fascinating and useful for my project. However, I'm currently facing a challenge and could use some guidance.

    I'm developing a Flask application that enables the sending of bulk emails using Flask and Flask-SocketIO. The application utilizes Flask to render a web page where users can fill out a form and upload relevant details. Upon form submission, the application performs necessary actions based on our environment. To trigger the email sending process on the client side, I'm utilizing Flask-SocketIO. This allows for the continuous transmission of email logs or history from the server to the client, which is functioning correctly.

    However, I encountered an issue when refreshing the page. Each time a refresh occurs, a new socket connection is established, resulting in a loss of email logs on the client side. I managed to address this problem to some extent with the following code snippet:

    @socketio.on('disconnect')
    def disconnect():
    global g_server_connected
    print("Disconnect")
    print(request.sid)
    g_server_connected = False

    @socketio.event
    def connect():
    print("Connected")
    global g_server_connected, g_background_process
    g_server_connected = True
    if g_background_process:
    message = {'message': "Connected with the server's background process", 'code': 200, 'counter': g_counter}
    emit("on_load_message", message)
    else:
    message = {'message': "Connected with the server", 'code': 200, 'counter': g_counter}
    emit("display_logs", message)

    The disconnect event handler is triggered when a client disconnects, updating g_server_connected accordingly. On the other hand, the connect event handler is executed when a client establishes a connection. It sets g_server_connected to True and emits the appropriate message to the client based on the g_background_process status.

    Although this solution works intermittently, it doesn't consistently provide the desired outcome. I would greatly appreciate any suggestions on how to overcome this issue or alternative approaches to effectively accomplish the task.

    Thank you in advance for your assistance.

  • #16 Miguel Grinberg said

    @Dilip: Unfortunately this is a difficult problem. Your application needs to be able to suspend and resume the context of each user. Your solution relies on global variables, which only allows you to maintain a single user context. You will need to implement some form of user authentication, and also use a database to store user information about the background task(s) associated with the user.

  • #17 Chandan said

    Hi @Miguel,

    <h2>I have socketio with background task(running in a while loop emitting continuous data) as below. Here "workhorse" function gets called on socketio connect and starts a while loop in background emitting data to client every 3 seconds(say, 1 minute CPU load). I am using Flask Login and Session for user login and upon login, user can see the CPU data being refreshed every 3 seconds. Now, if I have 2 sessions open (in 2 browser tabs) and if I logout from one of the browser tab, the other browser tab still keeps receiving the data from socketIO.</h2>

    @socketio.on('connect')
    @authenticated_only
    def myhandler(args, kwargs):
    socketio.sleep(1)
    with thread_lock:
    if thread is None:
    thread = socketio.start_background_task(workhorse,
    args, **kwargs )

    def workhorse(sid, args, *kwargs):
    while True:
    socketio.sleep(3)
    load = 'w|awk -F "load average:" \'{print$2}\'|awk -F "," \'{print$1}\''
    cpuload = subprocess.run(load,stdout=subprocess.PIPE,universal_newlines=True,shell=True)
    socketio.emit('myevent',{'data1': cpuoad.stdout}, room=sid)

    <hr />
  • #18 Miguel Grinberg said

    @Chandan: Once a WebSocket is established it is not interrupted, even if the user logs out on another tab. Authentication is checked when the Socket.IO connection is established, not every time an event is exchanged.

  • #19 David said

    Hi Miguel,

    From what I understand about your statements made about passing a user session, having been logged in via an HTTP endpoint, to websockets is that the session should pass along to websockets, whether or not I wish to keep the sessions identical or fork them.

    It appears that reviewing every example I have been able to see, that somewhere I seem to be unable pass that session to my websockets.

    In summary, my init.py and routes.py look like this (simplified):

    from flask import Flask
    from config import Config
    from flask_cors import CORS, cross_origin
    from flask_session import Session
    from flask_socketio import SocketIO
    from flask_login import LoginManager
    
    app = Flask(__name__, static_folder='../build', static_url_path='/')
    app.config.from_object(Config)
    app.secret_key = Config.SECRET_KEY
    app.config['SESSION_TYPE'] = Config.SESSION_TYPE
    CORS(app)
    login = LoginManager(app)
    Session(app)
    socketio = SocketIO(app, manage_session=False, cors_allowed_origins=Config.CORS_ORIGINS)
    
    if __name__ == '__main__':
        app.run(host="0.0.0.0", port=Config.PORT)
        socketio.run(app)
    
    @app.route('/api/v1/current_user', methods=['GET'])
    def get_user():
        print(current_user.is_authenticated) # returns True
    
    @socketio.on("get data")
    def get_data(data):
        print(current_user.is_authenticated) # returns False, and cannot access the attributes that I would be able to access if this were an HTTP endpoint
    

    Have you even run into this kind of situation, and if so, where have I strayed from your tutorial?

    All the best, David

  • #20 Miguel Grinberg said

    @David: the code does not include anything that is of interest to debug this problem. The issue is likely that the Socket.IO connection is established before the user logs in. You have to implement a login form first, then once the log in is accepted redirect to a page that makes the Socket.IO connection.

  • #21 David said

    In response your your message #20:

    I do indeed create a session by using flask-login first. I then check that this session exists by calling that /api/v1/current_user endpoint - it returns an current_user object instantiated from a User class I defined using sqlalchemy.

    It only seems that once I connect to socketio the session can no longer be accessed. I hope that clarifies my situation.

  • #22 Miguel Grinberg said

    @David: The Flask-SocketIO repository has a session example that you can use to test how sessions and logins work. Use that as a base to figure out what is the mistake in your application.

  • #23 David said

    I have indeed been following your examples closely and religiously - and perhaps my problems instead lie within how I connect? I don't know - but I have a React front end - and I know you're a python guy, but would you see there to be any problem with the following?

    import { io } from 'socket.io-client';
    import React, { Component } from 'react';
    class CampaignFieldView extends Component {
    
      constructor(props) {
        super(props);
        this.state = {}
      }
    
      componentDidMount() {
        fetch('/api/v1/current_user', {
          credentials: 'same-origin',
          method: 'GET',
          headers: { 'Content-Type':'application/json'}
        })
        .then(response =>
          response.json().then(body => ({
            body: body,
            status: response.status
          })
        ).then(res => {
          if(res.status === 200) {
            console.log(res.body);
          }
        }));
    
        let server;
        let domain;
        if (window.location.hostname === 'localhost') {
          server = `${window.location.hostname}:3000`;
          domain = `http://${window.location.hostname}:8000`;
        } else {
          server = window.location.hostname;
          domain = window.location.hostname;
        }
        socket = io(`http://${server}`, {
          cors: {
              origin: domain,
              methods: ["GET", "POST"],
              transports: ['websocket', 'polling'],
              credentials: true
          },
          allowEIO3: true
        });
    
        socket.emit("get map", { campaignId: this.props.campaign.id });
      }
    
      ...
    

    When I call the current_user endpoint, I can confirm that current_user exists and reflects who is currently logged in. But when I Emit and connect to socketio, on the flask side current user is not recognized and comes up as flask_login.mixins.AnonymousUserMixin. The python code that I have set up is precisely what I have posted in message #19. At this point, even a read-only access to current_user would be desirable. Any further thoughts?

  • #24 David said

    I hate to double-post here, but please disregard my message (what would have been #23, and this would be #24) -- I figured out my problem and it was simply that I wasn't sending the cookies to websockets from my client, using the package socket.io-client. I attempted to get by using the following code:

        socket = io(`http://${server}`, {
          cors: {
              origin: domain,
              methods: ["GET", "POST"],
              transports: ['websocket', 'polling'],
              credentials: true
          }
    

    but at the same level as the "cors" key, I needed to include withCredentials: true - that solved it. I thought by including credentials: true inside the "cors" key object value, that I would have been able to pass my cookies along.

Leave a Comment