miguelgrinberg / flask-sock

Modern WebSocket support for Flask.
MIT License
279 stars 23 forks source link

Connection always close with 1006 #52

Closed GiuseppeBocci closed 1 year ago

GiuseppeBocci commented 1 year ago

I'm trying to send push notifications to the client after the opening of the connection.

@sock.route('/sock')
def sock(ws):
    id= ws.receive()
    if id_exists(id):
        while condition:
            pass
            line = next_line()
            line = out.readline().strip()
            if line != "":
                ws.send(line)
                sleep(0.2)
            else:
                sleep(2)
        ws.send("pushing finished")
        ws.close(1000, message="FILES_READY")
        return abort(500)
    else:
        ws.close(reason=1008, message="BAD_REQUEST")

The client receive all the messages, but the connection always die with 1006 instead of closing and I don't understand why. I'm using it in debug mode. There is any other way to close the connection without aborting?

miguelgrinberg commented 1 year ago

What web server are you using? If you are using Werkzeug, I suggest you try a production web server to see if you get better results.

GiuseppeBocci commented 1 year ago

What web server are you using? If you are using Werkzeug, I suggest you try a production web server to see if you get better results.

Thanks for your quick response. I tried to use the gevent WSGI server, but the result is the same

GiuseppeBocci commented 1 year ago

Idk if it can help, but in the js console I get "loader.js:84 WebSocket connection to 'ws://127.0.0.1:5000/sock' failed: Invalid frame header" when the connection die.

This is my js code:

function openWebSocketCommunication(id){
    clientSocket = new WebSocket("ws://127.0.0.1:5000/sock"); // line 84
    console.log("Connection opening");

    clientSocket.onopen = (event) => {
        clientSocket.send(id);
        console.log("Websocket connection opened!");
    };

    clientSocket.onmessage = (event) => {
        console.log("Message: " + event.data);
    };

    clientSocket.onclose = function(event) {
        if (event.wasClean) {
            alert("Your results are ready!"+event.code+' reason='+event.reason)
        }
        else{
            alert('[close] Connection died! code='+event.code+' reason='+event.reason);
        }
    };
}
miguelgrinberg commented 1 year ago

Well, I cannot reproduce this here. I'm using the echo.py example from this repository, which allows you to type anything in the browser and the server replies back with the same text. If you type close, the server ends the websocket connection.

I've added code in the JavaScript side to show code value on the close event and I'm getting 1000, which is the expected. Here is the diff, in case you want to try this example:

diff --git a/examples/templates/index.html b/examples/templates/index.html
index 5fecab5..9e42d57 100644
--- a/examples/templates/index.html
+++ b/examples/templates/index.html
@@ -22,7 +22,7 @@
         log('<<< ' + ev.data, 'blue');
       });
       socket.addEventListener('close', ev => {
-        log('<<< closed');
+        log('<<< closed ' + ev.code + ' ' + ev.reason);
       });
       document.getElementById('form').onsubmit = ev => {
         ev.preventDefault();

I tested Chrome and Firefox, both return 1000 after I close the connection.

GiuseppeBocci commented 1 year ago

Looking at the examples I tried different options. I almost solved my problem(the connection is closed with the code and message I wanted):

Using Werkzeug:

I eliminated the return abort(xxx) and added a While True. Now, my code look like this:

@sock.route('/sock')
def sock(ws):
    while True:  # wrapping while added
        id= ws.receive()
        if id_exists(id):
            while condition:
                pass
                line = next_line()
                line = out.readline().strip()
                if line != "":
                    ws.send(line)
                    sleep(0.2)
                else:
                    sleep(2)
            ws.send("pushing finished")
            ws.close(1000, message="FILES_READY")   # return abort(500) deleted
        else:
            ws.close(reason=1008, message="BAD_REQUEST")

for what I can say a break it is not necessary. I also tried to modify the echo example with the server closing the connection and it works:

@sock.route('/echo')
def echo(ws):
    while True:
        data = ws.receive()
        if data == 'close':
            ws.close(1000, message="All ok")  # break not needed
        ws.send(data) 

Here is my question: "why do I need the while True to wrap the function's body?"

⚠️ Note - In the console the following lines are printed in both the examples:

127.0.0.1 - - code 400, message Bad request syntax ('\x88\x8d*%\x8a\x87)ÍÌÎf`ÙØx`ËÃs')
127.0.0.1 - - "None /sock HTTP/0.9" HTTPStatus.BAD_REQUEST -

Using gevent WSGI server:

For me it seems the problem was the monkey patching too late, so I moved it before all the imports, but sometimes the first execution does not work (returned code=1006). Using the wrapping while True seems to always work, but I'm not sure. ⚠️ Note - This error is printed in the console in both cases and in the echo example too :

gevent.exceptions.ConcurrentObjectUseError: This socket is already used by another greenlet: <bound method Waiter.switch of <gevent._gevent_c_waiter.Waiter object at 0x0000029C97723458>>
2023-04-13T09:48:07Z <Greenlet at 0x29c9771f1a8: _handle_and_close_when_done(<bound method WSGIServer.handle of <WSGIServer at , <bound method StreamServer.do_close of <WSGIServer, (<gevent._socket3.socket [closed] at 0x29c97716b38)> failed with ConcurrentObjectUseError

To your knowledge, does this error alter the run of the program? Is there anything I can do to prevent this error?

miguelgrinberg commented 1 year ago

Okay, couple of things. First of all, the break is necessary. Take a look at this version of the code that you are using:

@sock.route('/echo')
def echo(ws):
    while True:
        data = ws.receive()
        if data == 'close':
            ws.close(1000, message="All ok")  # break not needed
        ws.send(data) 

Here you would be closing the socket, and then a ws.send() call will be made. Why would you want to do that? This call will likely end with an error since the socket is now closed. The break is there to gracefully exit the while loop.

why do I need the while True to wrap the function's body?

Because without the while loop the websocket route will exit and close the connection after the first interaction with the client, instead of waiting for user input.

⚠️ Note - In the console the following lines are printed in both the examples:

Not really sure what code you are running that causes this, and what version of Werkzeug. I do not see these messages here when using the echo example app.

Regarding the Gevent tests, are you using Gevent's own web server or Gunicorn? I rarely use Gevent's web server, but don't recall seeing strange errors with Gunicorn.

GiuseppeBocci commented 1 year ago

⚠️ Note - In the console the following lines are printed in both the examples: Not really sure what code you are running that causes this, and what version of Werkzeug. I do not see these messages here when using the echo example app.

About the first note: I get this message from the normal example echo app too. My version of Werkzeug is 2.2.2 .

why do I need the while True to wrap the function's body?

Here I wasn't referring to the echo example. This happens to me:

Here you would be closing the socket, and then a ws.send() call will be made. Why would you want to do that? This call will likely end with an error since the socket is now closed. The break is there to gracefully exit the while loop.

For me, the ws.close() also breaks from the while True, in ALL the past examples. Given this code:

@sock.route('/echo')
def echo(ws):
    while True:
        data = ws.receive()
        if data == 'close':
            ws.close(1000)
        print("hello1")
        ws.send("hello2")
        ws.send(data)

In the page, I get the following output: image In the prompt, I get:

127.0.0.1 - - [13/Apr/2023 16:10:26] "GET / HTTP/1.1" 200 -
hello1
hello1

with no error other than:

127.0.0.1 - - [13/Apr/2023 16:10:36] code 400, message Bad request syntax ('\x88\x820\x18;Þ3ð') 127.0.0.1 - - [13/Apr/2023 16:10:36] "None /echo HTTP/0.9" HTTPStatus.BAD_REQUEST -

And I get no error in the javascript console, so I deduced that the ws.close() also breaks from the while True.

Regarding the Gevent tests, are you using Gevent's own web server or Gunicorn? I rarely use Gevent's web server, but don't recall seeing strange errors with Gunicorn.

I'm using Gevent's own web server. I'll try Gunicorn and let you know.

Sorry if I didn't explain the problem well. I hope it's clearer this time, please let me know if any further clarification is needed.

miguelgrinberg commented 1 year ago

I get this message from the normal example echo app too. My version of Werkzeug is 2.2.2 .

Okay, I see the 400 now. Werkzeug 2.0.x does not show it, but 2.1.x and 2.2.x do, so they must have changed something that affects the ending of the WebSocket connection.

If I don't use a while True loop the connection always ends with the code 1006.

I'm sorry but I still don't understand what you mean here. If you don't use a while loop, how do you prevent the websocket route from exiting? The connection closes when the route function ends so you need something to keep the function running.

For me, the ws.close() also breaks from the while True, in ALL the past examples.

The close() has nothing in it that breaks a loop. It does not even know if it is called inside or outside a loop. What happens is that something that you do after the close call errors due to the socket being closed, and that causes the websocket route to abort. It's a bad practice, and completely unintuitive to rely on this indirect behavior. If you want my advice, I would break the loop gracefully and call close outside the loop at the end.

GiuseppeBocci commented 1 year ago

If you want my advice, I would break the loop gracefully and call close outside the loop at the end.

Thank you. I'll follow your advice.

If you don't use a while loop, how do you prevent the websocket route from exiting? The connection closes when the route function ends so you need something to keep the function running.

In my project, I send to the client via websocket push notifications about the progress of another python script executed using subprocess. In the end, we can simplify by saying that when the client connects I want to send the numbers from 1 to 100 and I want to end the connection with the reason(here called message): "all ok". You can see that for the behavior of this program, I don't need an infinite loop. While I was preparing the example:

@sock.route('/foo')
def foo(ws):
   name = ws.receive()
   ws.send("hello " + name + "!")
   for i in range(1, 101):
      ws.send(i)
   ws.close(message="all ok")  # default code(reason) = 1000

I set up a new environment, and my original program worked as well. So I conclude something, was modified in the old environment, but I don't understand what. I hope this issue is still useful for new examples, best practices, and the behavior of Werkzeug >= 2.1.x and Gevent's web server. Thanks for your openness and answers.