Websocket from Dash Extensions does not work once deployed (on Heroku)

Hi there, @Emil @jokin first of all thanks a lot for those amazing dash-extensions websocket releases :clap:

We’re an open-source projet trying hard to have our platform processing smoothly our API Post data. We are then using the Websocket component and it’s working pretty fine as long as we’re on http://localhost:8050/

Here is a slight snapshot of how our app interact with the websocket

main.py →

# Main Dash imports, used to instantiate the web-app and create callbacks (ie. to generate interactivity)

import os

import dash

from dash.dependencies import Input, Output, State, MATCH

from dash.exceptions import PreventUpdate

# Flask caching import

from flask_caching import Cache

# Various modules provided by Dash and Dash Leaflet to build the page layout

import dash_core_components as dcc

import dash_html_components as html

import dash_bootstrap_components as dbc

import dash_leaflet as dl

from dash_extensions.websockets import SocketPool, run_server

from dash_extensions import WebSocket

# Pandas to read the login correspondences file

import pandas as pd

# Import used to make the API call in the login callback

import requests


# ----------------------------------------------------------------------------------------------------------------------
# APP INSTANTIATION & OVERALL LAYOUT

# We start by instantiating the app (NB: did not try to look for other stylesheets yet)
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.UNITED])
socket_pool = SocketPool(app)

# We define a few attributes of the app object
app.title = 'Pyronear - Monitoring platform'
app.config.suppress_callback_exceptions = True
server = app.server  # Gunicorn will be looking for the server attribute of this module

# We create a rough layout, filled with the content of the homepage/alert page
app.layout = html.Div(
    [
        dcc.Location(id="url", refresh=False),
        html.Div(id="page-content", style={"height": "100%"}),

        # Storage components which contains data relative to alerts
        dcc.Store(id="store_live_alerts_data", storage_type="session"),
        dcc.Store(id="last_displayed_event_id", storage_type="session"),
        dcc.Store(id="images_url_live_alerts", storage_type="session", data={}),
        # Storage component which contains data relative to site devices
        dcc.Store(id="site_devices_data_storage", storage_type="session", data=get_site_devices_data(client=api_client))
    ]
)

# End point ping by the API for each alert being recorded. Then broadcasting message to ALL sessions
@app.server.route("/alert/<message>")
def broadcast_message(message):
    socket_pool.broadcast(message)
    return f"Message {message} broadcast."

# First API relay once an alert is sent to the platform, passing msg.data into msg hidden div
app.clientside_callback("function(msg){if(msg == null) {return;} else {return msg.data;}}",
                        Output("msg", "children"), [Input("ws", "message")])

@app.callback(
    Output('store_live_alerts_data', 'data'),
    Output('images_url_live_alerts', 'data'),
    Input('msg', 'children')
)
def update_live_alerts_data(alert):
    # Fetching live alerts where is_acknowledged is False
    response = api_client.get_ongoing_alerts().json()
    all_alerts = pd.DataFrame(response)
    if all_alerts.empty:
        raise PreventUpdate
    live_alerts = all_alerts.loc[~all_alerts["is_acknowledged"]]

    # Fetching live_alerts frames urls and instantiating a dict of live_alerts urls having event_id keys
    dict_images_url_live_alerts = {}
    for _, row in live_alerts.iterrows():
        img_url = ""
        try:
            img_url = api_client.get_media_url(row["media_id"]).json()["url"]
        except Exception:
            pass
        if row['event_id'] not in dict_images_url_live_alerts.keys():
            dict_images_url_live_alerts[row['event_id']] = []
            dict_images_url_live_alerts[row['event_id']].append(img_url)
        else:
            dict_images_url_live_alerts[row['event_id']].append(img_url)

    return live_alerts.to_json(orient='records'), dict_images_url_live_alerts    

if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser(description='Pyronear web-app',
                                     formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    parser.add_argument('--host', type=str, default='127.0.0.1', help='Host of the server')
    parser.add_argument('--port', type=int, default=8050, help='Port to run the server on')
    args = parser.parse_args()

    run_server(app, port=args.port)

If my understanding is OK the WebSocket(id="ws") (located in another homepage.py) is updated either by the app.clientside_callback or socket_pool.broadcast(message)

Anyway everything is working fine, each time we GET the route e.g : requests.get("http://localhost:8050/alert/16") the callback chain is triggered.

Things get more tricky when the app is deployed on heroku, we actually deploy modifying the Websocket component to make it possible to use https as follow WebSocket(url=“wss://pyro-platform.herokuapp.com/”, id=“ws”). The connection seems to work but our callbacks are not triggered anymore when requests.get("http://pyro-platform.herokuapp.com/alert/12")

I know it’s long but if you could help it would be awesome, full code here btw, thankksssss :pray:

NB : no errors neither among heroku logs nor browser console

Hi. Did you manage to solve this issue on a production server? I am trying to deploy the Websockets from @Emil on gunicorn to handle some long tasks (our servers are in Hong Kong but our operations team is in India; for some long operations, the Indian ISPs reset the connection to the server before it can be completed, hence the migration to websockets).

In our implementation (which runs fine without websockets), we use:

  • the flask server to route for authentication:
    @server.route(’/’, methods=[‘GET’])
    def say_hello():
    from flask import request

    assertion = request.headers.get(‘X-Goog-IAP-JWT-Assertion’)
    email, id = validate_assertion(assertion)
    return app.index()

  • Launch the dash app post authentication using app.index() … works like a charm

After putting in websockets into production using gunicorn, the socket fails. I have tried using flask workers when starting the flask servers but that messes up Dash.

If anyone has any pointers here, it would be much appreciated.