miguelgrinberg / flask-sock

Modern WebSocket support for Flask.
MIT License
272 stars 24 forks source link

invalid frame header #67

Open asafm-DI opened 11 months ago

asafm-DI commented 11 months ago

I am trying to upgrade ,an application i am working on from flask-socket to flask-sock a while ago flask and werkzeug were upgraded from very old versions to 2.2.2 for both and caused the websocket to didn't work.

Following the gevent example works on an isolated environment, while at my app, during the connection handshake, the underlaying simple-websocket sends an AcceptConnection which resulted with error (1002 protocol error).

The original request:

Request(host='localhost:5001', target='/', extensions=['permessage-deflate; client_max_window_bits'], extra_headers=[(b'connection', b'Upgrade'), (b'pragma', b'no-cache'), (b'cache-control', b'no-cache'), (b'user-agent', b'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36'), (b'upgrade', b'websocket'), (b'origin', b'https://www.piesocket.com'), (b'sec-websocket-version', b'13'), (b'accept-encoding', b'gzip, deflate, br'), (b'accept-language', b'en-US,en;q=0.9'), (b'sec-websocket-key', b'Jj7EU6WXADUUFno6ByyM8Q==')], subprotocols=[])

The response:

InformationalResponse(headers=<Headers([(b'upgrade', b'WebSocket'), (b'connection', b'Upgrade'), (b'sec-websocket-accept', b'zAzLyNKKjek9I3n8P/okYVfJPVU='), (b'sec-websocket-extensions', b'permessage-deflate; client_max_window_bits=15')])>, http_version=b'1.1', reason=b'', status_code=101)

At this point the client (tried multiple ones) sees the following error: Invalid frame header

While the flask server returns

File "/Users/user/workspace/Management/server/src/management/modules/websocket/module.py", line 268, in _read_from_websocket
    s=websocket_connection.receive(),
  File "/Users/user/.local/share/virtualenvs/management_web_api_server-gHGDlQHu/lib/python3.7/site-packages/simple_websocket/ws.py", line 112, in receive
    raise ConnectionClosed(self.close_reason, self.close_message)
simple_websocket.ws.ConnectionClosed: Connection closed: 1002 WebSocket Protocol Error

I tried to debug both raw example and my app, both send the exact same AcceptConnection response (only difference was the token's value). however my application closes the connection during the handshake. I couldn't find evidence for a middleware which messes with the headers/buffer.

I did find this comment.

My question is if there is a support for flask and werkzeug 2.2.2 with this library and if there is a reason for the flask route to reject a message sent during the handshake

miguelgrinberg commented 11 months ago

Can you please create a short and simple example application that demonstrates the issue? The latest Flask and Werkzeug versions should be supported.

asafm-DI commented 11 months ago

yes, i didn't manage to reproduce it outside of the legacy application, i am still trying to reproduce it via a simple app which will also help me isolate the issue. I also try to upgrade flask to latest which may take some time

asafm-DI commented 11 months ago

The difference in the flows is after the accept connection (which is the same for both success and failure applications), the server sends 'b'\x88\x1a\x03\xeaWebSocket Protocol Error'' to the client

miguelgrinberg commented 11 months ago

I think it is going to be very difficult to debug this just by looking at the bytes. What the server sends is a standard protocol error. What matters is what made the server send this error, which must have happened before.

asafm-DI commented 11 months ago

is there a focal point to find what happens after the 'accept connection' phase?

miguelgrinberg commented 11 months ago

The WebSocket logic is not in this repository, you will need to look at the wsproto package, where the low-level protocol handshakes are implemented.

barv-di commented 9 months ago

Hi, in continue to asafm-DI issue I propose a websocket app which doesn't work, this (hopefully) imitates our way of work and perhaps would be easier for you to understand.

import geventwebsocket.handler
from flask import Flask, render_template
from flask_sock import Sock
from gevent.pywsgi import WSGIServer

app = Flask(__name__)
sock = Sock(app)

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

def _register_websocket_endpoints():
    @sock.route('/echo')
    def echo(ws):
        _handle_websocket_connection(ws)

def _handle_websocket_connection(websocket_connection):
    while True:
        websocket_connection.receive()
        websocket_connection.send("bar")

if __name__ == "__main__":
    _register_websocket_endpoints()
    WSGIServer(('0.0.0.0', 11206), app, handler_class=geventwebsocket.handler.WebSocketHandler,
               ).serve_forever()

We can see the error here - it says the connection was closed but we are not sure why, we assume it is because we are not proficient at using the new flask-sock and we would use help.

Screenshot 2023-10-12 at 17 41 26

Thank you

miguelgrinberg commented 9 months ago

@barv-di There is a gevent example in this repo. You are not doing this correctly. Two problems, a) you need to monkey patch the standard library for Gevent compatibility and b) you should not use gevent-websocket with this package. Your fixed example would be:

from gevent import monkey
monkey.patch_all()

from flask import Flask, render_template
from flask_sock import Sock
from gevent.pywsgi import WSGIServer

app = Flask(__name__)
app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 25}
sock = Sock(app)

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

def _register_websocket_endpoints():
    @sock.route('/echo')
    def echo(ws):
        _handle_websocket_connection(ws)

def _handle_websocket_connection(websocket_connection):
    while True:
        websocket_connection.receive()
        websocket_connection.send("bar")

if __name__ == "__main__":
    _register_websocket_endpoints()
    WSGIServer(('0.0.0.0', 11206), app).serve_forever()
barv-di commented 9 months ago
from gevent import monkey
monkey.patch_all()

from flask import Flask, render_template
from flask_sock import Sock
from gevent.pywsgi import WSGIServer

app = Flask(__name__)
app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 25}
sock = Sock(app)

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

def _register_websocket_endpoints():
    @sock.route('/echo')
    def echo(ws):
        _handle_websocket_connection(ws)

def _handle_websocket_connection(websocket_connection):
    while True:
        websocket_connection.receive()
        websocket_connection.send("bar")

if __name__ == "__main__":
    _register_websocket_endpoints()
    WSGIServer(('0.0.0.0', 11206), app).serve_forever()

Running this still not works (closing state) the same error. Running it with - gunicorn -b 0.0.0.0:11206 --workers 4 --threads 100 --worker-class gevent app:app

miguelgrinberg commented 9 months ago

See the documentation for how to run with gunicorn: https://flask-sock.readthedocs.io/en/latest/web_servers.html#id3

barv-di commented 9 months ago

Running the code in previous post along with gunicorn -b 0.0.0.0:11206 --worker-class gevent app:app does not work. the socket is in closed state (same error as before when sending a message in the index.html popup) I copied the code you had written and the command from ur documentation.

I attach the pip3 list output (maybe useful?)

`Package          Version
---------------- -------
bidict           0.22.1
blinker          1.6.3
click            8.1.7
dnspython        2.4.2
eventlet         0.33.3
Flask            2.3.2
flask-sock       0.7.0
Flask-SocketIO   5.3.6
gevent           23.9.1
greenlet         3.0.0
gunicorn         21.2.0
h11              0.14.0
itsdangerous     2.1.2
Jinja2           3.1.2
MarkupSafe       2.1.3
packaging        23.2
pip              23.2.1
python-engineio  4.7.1
python-socketio  5.9.0
setuptools       68.2.2
simple-websocket 1.0.0
six              1.16.0
Werkzeug         3.0.0
wheel            0.41.2
wsproto          1.2.0
zope.event       5.0
zope.interface   6.1
`
miguelgrinberg commented 9 months ago

The code that I shown above is written to be used with the gevent web server, not gunicorn. I based it on your own code. Does it work that way?

Because you are registering your endpoints inside the if condition at the bottom. That code does not run when Gunicorn imports your app.

erikpa1 commented 9 months ago

@miguelgrinberg I'm joining this conversation, there is random behaviour with error:

WebSocketApi.ts:3 WebSocket connection to 'ws://localhost:5000/ws' failed: Invalid frame header

Error is shown after first send (from client) and receiving message (from server).

Browser javascript code:

export class WebSocketApi {

    static SOCKET: WebSocket | any = new WebSocket("ws://localhost:5000/ws")

    static ONCONNECT = new Map<string, any>()
    static ONMESSAGE = new Map<string, Map<string, any>>()

    static Init() {
        WebSocketApi.SOCKET.onopen = WebSocketApi._Connected
        WebSocketApi.SOCKET.onmessage = WebSocketApi._OnMessage

        WebSocketApi.SOCKET.onerror = (e) => {
            console.error(e)
        }

    static SendMessage(message: string) {
        WebSocketApi.SOCKET.send(message)
    }

 }
}

flask code

from flask_cors import CORS from flask_sock import Sock from flask import Flask

app = Flask("Exear", static_url_path='') app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{os.path.join(vfs.GetWorkingDirectory(), 'vts_db.db')}" app.config['SECRET_KEY'] = 'secretkeytopsecret'

websocket = Sock(app)

@websocket.route("/ws") def _WsReturn(ws: wsServer):

print("---------")
print(ws.receive())
ws.send(f"Respose {random.randint(0, 10)}")

app.run(debug=True, host="0.0.0.0")

miguelgrinberg commented 9 months ago

@erikpa1 Try with a production web server. You are using the Flask development web server for this and on top of that you are using development mode in your app. Also your websocket handler exits after receiving and sending. Is this intended? The connection will close if you let the function end.

erikpa1 commented 9 months ago

@miguelgrinberg yeah sorry, I came to write, it was because I was missing while True, sorry :D my mistake :D