Repository: sanic-org/sanic

Issue:
websocket disconnect  every minute and raise 503 error.
**Describe the bug**
from client, every 25 seconds send a "ping",
on server side, when received "ping", send a "pong".

on client side, after sent a "ping", wait 15 seconds, if not receive "pong", then reconnect.

websocket disconnect  about every minute.

**Code snippet**
server.py:
```python
from sanic import Sanic
app = Sanic("websocket_example")
@app.websocket('/wsserver')
async def feed(request, ws):
    while True:
        data = 'pong'
        print('Sending: ' + data)
        await ws.send(data)
        data = await ws.recv()
        print('Received: ' + data)
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8001)

```

**Expected behavior**
keep connection!

**Environment (please complete the following information):**
 - OS: ubuntu20.04
 - Version : sanic 21.3.1


**Additional context**
on server side log:
```
[2021-03-22 06:28:15 +0800] [3380798] [INFO] Goin' Fast @ http://0.0.0.0:8001
[2021-03-22 06:28:15 +0800] [3380798] [INFO] Starting worker [3380798]
Sending: pong
Received: ping
Sending: pong
Received: ping
Sending: pong
Received: ping
Sending: pong
[2021-03-22 06:29:35 +0800] - (sanic.access)[INFO][42.185.74.69:34866]: GET ws://42.3.27.246/wsserver  503 -1
Sending: pong
Received: ping
Sending: pong
Received: ping
Sending: pong
Received: ping
Sending: pong
[2021-03-22 06:30:52 +0800] - (sanic.access)[INFO][42.185.74.69:34911]: GET ws://42.3.27.246/wsserver  503 -1
Sending: pong
Received: ping
Sending: pong
Received: ping
Sending: pong
Received: ping
Sending: pong
[2021-03-22 06:32:09 +0800] - (sanic.access)[INFO][42.185.74.69:35021]: GET ws://42.3.27.246/wsserver  503 -1

```


Discussion:
I have test some different codes, the same error on them.

I have no idea now, please check if it is a bug or my code is wrong?

anyone faced the same issue?
It is tough to say without seeing your client code as well. One of the first thing that jumps out at me is that you have both the consumer (`ws.recv()`) and the producer (`ws.send()`) running consecutively. Therefore, each of those operations will block the other from happening. The handler will pause at each `await` until it can be resolved. Depending upon your client logic, that might cause the described behavior.
thanks for reply.

I am almost certain it is a bug in sanic new version  21.3.0 and 21.3.1.

on sanic 21.12.0-21.12.3, the codes work smoothly.

please have a deep check.


 
Can you provide your client code?
the client very simple: 
1\every 25 seconds send a "ping",
2\after sent a "ping", wait 15 seconds, if not receive "pong", then reconnect.

the  (ws.recv()) and  (ws.send()) running consecutively, but it is copy from your official documents.

now I add a new logger about reconnection to show the bug clearly. 

`
@app.websocket("/wsserver")
async def feed(request, ws):
    try:

        logger.debug(f"the client re-connected!")

        while True:

            #print("request:　", request)
            #print("ws:　", ws)

            name = await ws.recv()
            print(f"< {name}")

            greeting = f"pong"

            await ws.send(greeting)
            print(f"> {greeting}")

    except websockets.exceptions.ConnectionClosedOK:
        logger.debug("/wsserver ConnectionClosedOK")
    except websockets.exceptions.ConnectionClosedError:
        logger.debug("/wsserver ConnectionClosedError")
    except sanic.websocket.ConnectionClosed:
        logger.debug("/wsserver ConnectionClosed: " + str(e))
    except websockets.exceptions.WebSocketException as e:
        logger.debug("/wsserver WebSocketException: " + str(e))
    except concurrent.futures.CancelledError:
        logger.debug("/wsserver CancelledError")
    except BaseException as e:
        logger.debug("/wsserver BaseException: " + str(type(e)))
`


```

[2021-03-22 13:07:35 +0800] [3463351] [INFO] Goin' Fast @ http://0.0.0.0:8001
[2021-03-22 13:07:35 +0800] [3463351] [INFO] Starting worker [3463351]
[2021-03-22 13:07:40 +0800] [3463351] [DEBUG] the client re-connected!
< ping
> pong
< ping
> pong
< ping
> pong
[2021-03-22 13:08:42 +0800] [3463351] [DEBUG] /wsserver BaseException: <class 'asyncio.exceptions.CancelledError'>
[2021-03-22 13:08:43 +0800] - (sanic.access)[INFO][104.233.191.163:34936]: GET ws://42.3.27.246/wsserver  503 -1
[2021-03-22 13:08:43 +0800] [3463351] [ERROR] Connection lost before response written @ ('104.233.191.163', 34936) <Request: GET /wsserver>
[2021-03-22 13:08:54 +0800] [3463351] [DEBUG] the client re-connected!
[2021-03-22 13:09:05 +0800] [3463351] [DEBUG] /wsserver BaseException: <class 'asyncio.exceptions.CancelledError'>
[2021-03-22 13:09:05 +0800] [3463351] [ERROR] Connection lost before response written @ ('104.233.191.163', 35081) <Request: GET /wsserver>
[2021-03-22 13:09:06 +0800] [3463351] [DEBUG] the client re-connected!
[2021-03-22 13:09:17 +0800] [3463351] [DEBUG] /wsserver BaseException: <class 'asyncio.exceptions.CancelledError'>
[2021-03-22 13:09:17 +0800] [3463351] [ERROR] Connection lost before response written @ ('104.233.191.163', 35100) <Request: GET /wsserver>
[2021-03-22 13:09:17 +0800] [3463351] [DEBUG] the client re-connected!
[2021-03-22 13:09:28 +0800] [3463351] [DEBUG] /wsserver BaseException: <class 'asyncio.exceptions.CancelledError'>
[2021-03-22 13:09:28 +0800] [3463351] [ERROR] Connection lost before response written @ ('104.233.191.163', 35107) <Request: GET /wsserver>
```


**Notice: the new codes work OK in version 20.12.3 too.!**

Resolving patch:
diff --git a/sanic/server.py b/sanic/server.py
--- a/sanic/server.py
+++ b/sanic/server.py
@@ -234,11 +234,16 @@ def check_timeouts(self):
             if stage is Stage.IDLE and duration > self.keep_alive_timeout:
                 logger.debug("KeepAlive Timeout. Closing connection.")
             elif stage is Stage.REQUEST and duration > self.request_timeout:
+                logger.debug("Request Timeout. Closing connection.")
                 self._http.exception = RequestTimeout("Request Timeout")
+            elif stage is Stage.HANDLER and self._http.upgrade_websocket:
+                logger.debug("Handling websocket. Timeouts disabled.")
+                return
             elif (
                 stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED)
                 and duration > self.response_timeout
             ):
+                logger.debug("Response Timeout. Closing connection.")
                 self._http.exception = ServiceUnavailable("Response Timeout")
             else:
                 interval = (


Test patch:
diff --git a/tests/test_response_timeout.py b/tests/test_response_timeout.py
--- a/tests/test_response_timeout.py
+++ b/tests/test_response_timeout.py
@@ -1,7 +1,11 @@
 import asyncio
+import logging
+
+from time import sleep
 
 from sanic import Sanic
 from sanic.exceptions import ServiceUnavailable
+from sanic.log import LOGGING_CONFIG_DEFAULTS
 from sanic.response import text
 
 
@@ -13,6 +17,8 @@
 response_timeout_default_app.config.RESPONSE_TIMEOUT = 1
 response_handler_cancelled_app.config.RESPONSE_TIMEOUT = 1
 
+response_handler_cancelled_app.ctx.flag = False
+
 
 @response_timeout_app.route("/1")
 async def handler_1(request):
@@ -25,32 +31,17 @@ def handler_exception(request, exception):
     return text("Response Timeout from error_handler.", 503)
 
 
-def test_server_error_response_timeout():
-    request, response = response_timeout_app.test_client.get("/1")
-    assert response.status == 503
-    assert response.text == "Response Timeout from error_handler."
-
-
 @response_timeout_default_app.route("/1")
 async def handler_2(request):
     await asyncio.sleep(2)
     return text("OK")
 
 
-def test_default_server_error_response_timeout():
-    request, response = response_timeout_default_app.test_client.get("/1")
-    assert response.status == 503
-    assert "Response Timeout" in response.text
-
-
-response_handler_cancelled_app.flag = False
-
-
 @response_handler_cancelled_app.exception(asyncio.CancelledError)
 def handler_cancelled(request, exception):
     # If we get a CancelledError, it means sanic has already sent a response,
     # we should not ever have to handle a CancelledError.
-    response_handler_cancelled_app.flag = True
+    response_handler_cancelled_app.ctx.flag = True
     return text("App received CancelledError!", 500)
     # The client will never receive this response, because the socket
     # is already closed when we get a CancelledError.
@@ -62,8 +53,44 @@ async def handler_3(request):
     return text("OK")
 
 
+def test_server_error_response_timeout():
+    request, response = response_timeout_app.test_client.get("/1")
+    assert response.status == 503
+    assert response.text == "Response Timeout from error_handler."
+
+
+def test_default_server_error_response_timeout():
+    request, response = response_timeout_default_app.test_client.get("/1")
+    assert response.status == 503
+    assert "Response Timeout" in response.text
+
+
 def test_response_handler_cancelled():
     request, response = response_handler_cancelled_app.test_client.get("/1")
     assert response.status == 503
     assert "Response Timeout" in response.text
-    assert response_handler_cancelled_app.flag is False
+    assert response_handler_cancelled_app.ctx.flag is False
+
+
+def test_response_timeout_not_applied(caplog):
+    modified_config = LOGGING_CONFIG_DEFAULTS
+    modified_config["loggers"]["sanic.root"]["level"] = "DEBUG"
+
+    app = Sanic("test_logging", log_config=modified_config)
+    app.config.RESPONSE_TIMEOUT = 1
+    app.ctx.event = asyncio.Event()
+
+    @app.websocket("/ws")
+    async def ws_handler(request, ws):
+        sleep(2)
+        await asyncio.sleep(0)
+        request.app.ctx.event.set()
+
+    with caplog.at_level(logging.DEBUG):
+        _ = app.test_client.websocket("/ws")
+    assert app.ctx.event.is_set()
+    assert (
+        "sanic.root",
+        10,
+        "Handling websocket. Timeouts disabled.",
+    ) in caplog.record_tuples