Repository: sanic-org/sanic

Issue:
asyncio.exceptions.CancelledError On Windows OS
The following exception raised when running the hello world example on Windows with Microsoft store version of Python 3.9.

WSL2 environment does not reproduce the issue.

```
from sanic import Sanic
from sanic.response import json

app = Sanic("My Hello, world app")

@app.route('/')
async def test(request):
    return json({'hello': 'world'})

if __name__ == '__main__':
    app.run()
```

```
[2021-06-30 23:00:05 -0700] [12184] [INFO] Goin' Fast @ http://127.0.0.1:8000
[2021-06-30 23:00:06 -0700] [12184] [INFO] Starting worker [12184]
[2021-06-30 23:00:11 -0700] - (sanic.access)[INFO][127.0.0.1:53159]: GET http://127.0.0.1:8000/  200 17
[2021-06-30 23:01:11 -0700] [12184] [ERROR] Exception occurred while handling uri: 'http:///*'
Traceback (most recent call last):
  File "C:\Users\zhiwe\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\sanic\http.py", line 126, in http1
    await self.http1_request_header()
  File "C:\Users\zhiwe\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\sanic\http.py", line 188, in http1_request_header      
    await self._receive_more()
  File "C:\Users\zhiwe\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\LocalCache\local-packages\Python39\site-packages\sanic\server.py", line 222, in receive_more
    await self._data_received.wait()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.9_3.9.1776.0_x64__qbz5n2kfra8p0\lib\asyncio\locks.py", line 226, in wait
    await fut
asyncio.exceptions.CancelledError
[2021-06-30 23:01:11 -0700] - (sanic.access)[INFO][UNKNOWN]: NONE http:///*  408 664
```


Discussion:
Not able to reproduce after a while, maybe it is a random error?
Looks like request timeout after 60 seconds. Did your client send a request and get a response OK? In that case, it was probably just an idling connection being terminated (but one ugly message for doing so, that still needs to be fixed).

If it were a request timeout, there should be `RequestTimeout` error.
Yes, something goes wrong in the error handling. But it happens after 60 seconds and logs a 408 Request Timeout status.

@Tronic I used browser to visit the endpoint, and it got the correct response. It seems not a timeout issue because the exception raised immediately after the request.
I would like to look into it while trying to understand how Python sanic is working with asyncio. Any suggestion about how to fix this? After reading the code in `server.py` and `http.py`, I think I got the basic concept of the data reading workflow, but I still don't understand why, how, and where the operation is cancelled... 
```python
    def check_timeouts(self):
        """
        Runs itself periodically to enforce any expired timeouts.
        """
        try:
            if not self._task:
                return
            duration = current_time() - self._time
            stage = self._http.stage
            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 = (
                    min(
                        self.keep_alive_timeout,
                        self.request_timeout,
                        self.response_timeout,
                    )
                    / 2
                )
                self.loop.call_later(max(0.1, interval), self.check_timeouts)
                return
            self._task.cancel()
        except Exception:
            error_logger.exception("protocol.check_timeouts")
```

server.py runs this background task that runs periodically checks if any timeouts have expired, then cancels `self._task` that handles the connection (server.py function `connection_task` that calls http.py and other functions). The `connection_task` function is supposed to catch and ignore CancelledError, so I would look into why you still get an error message. From the backtrace it looks as if `http1_request_header` is catching and logging the error, which is probably unintentional.

Happy hacking!
> @Tronic I used browser to visit the endpoint, and it got the correct response. It seems not a timeout issue because the exception raised immediately after the request.

Did you visit it twice, or just at 23:00:11 where a 200 OK response is logged? The remaining log events at 23:01:11 suggest that no request or possibly an incomplete request was received, but that no valid responses were served. If there was an incomplete request, Sanic tries to respond with 408, otherwise it just closes the socket.

It looks like that a connection was left idling by the browser after the initial and successful request in case it would do more requests, and Sanic closing the connection would cause no error in browser. If this is the case, Sanic behaved correctly but it should not be logging that exception or the 408 access log event.

> > @Tronic I used browser to visit the endpoint, and it got the correct response. It seems not a timeout issue because the exception raised immediately after the request.
> 
> Did you visit it twice, or just at 23:00:11 where a 200 OK response is logged? The remaining log events at 23:01:11 suggest that no request or possibly an incomplete request was received, but that no valid responses were served. If there was an incomplete request, Sanic tries to respond with 408, otherwise it just closes the socket.
> 
> It looks like that a connection was left idling by the browser after the initial and successful request in case it would do more requests, and Sanic closing the connection would cause no error in browser. If this is the case, Sanic behaved correctly but it should not be logging that exception or the 408 access log event.

I tried it again several times and the error popup after 1 min, so I think you are right. Let's see if we can catch that exception so it will not be raised.
@Tronic I found that the error was logged because there is not a error handler registered in ErrorHandler object's `cached_handlers`. Do you think this is an intentional behavior?
Ah, this might be related to another new feature with error handlers that recently got merged and released in 21.6 I think. Ping @ahopkins shouldn't custom error handlers be limited to `Exception` and not used `BaseException` (like the cancellation that occurs here)?

Ahh, that might make sense. I'm going to be pushing out a patch in Sunday so I will take a closer look at this.
Turns out the issue stems from #2077. The issue is related to how `quiet` is being set. This is probably worthy of a fix and release.

This is real easy to reproduce.

```python
@app.get("/")
async def handler(request):
    await asyncio.sleep(3)
```

Kill the request before the 3 seconds.
> Ping @ahopkins shouldn't custom error handlers be limited to `Exception` and not used `BaseException` (like the cancellation that occurs here)?

It is just looping through the hierarchy and stopping when it gets to `Exception` (or `CancelledError`, etc).
> Turns out the issue stems from #2077. The issue is related to how `quiet` is being set. This is probably worthy of a fix and release.
> 
> This is real easy to reproduce.
> 
> ```python
> @app.get("/")
> async def handler(request):
>     await asyncio.sleep(3)
> ```
> 
> Kill the request before the 3 seconds.

I think there is probably not `quite` attr in `CancelledError` object by default since it is not inherited from `SanicException`. Maybe we can consider replace `CancelledError` to some other Exception that is a sub-class `SanicException` in the first layer (http.py).

Resolving patch:
diff --git a/sanic/exceptions.py b/sanic/exceptions.py
--- a/sanic/exceptions.py
+++ b/sanic/exceptions.py
@@ -31,6 +31,7 @@ class NotFound(SanicException):
     """
 
     status_code = 404
+    quiet = True
 
 
 class InvalidUsage(SanicException):
@@ -39,6 +40,7 @@ class InvalidUsage(SanicException):
     """
 
     status_code = 400
+    quiet = True
 
 
 class MethodNotSupported(SanicException):
@@ -47,6 +49,7 @@ class MethodNotSupported(SanicException):
     """
 
     status_code = 405
+    quiet = True
 
     def __init__(self, message, method, allowed_methods):
         super().__init__(message)
@@ -70,6 +73,7 @@ class ServiceUnavailable(SanicException):
     """
 
     status_code = 503
+    quiet = True
 
 
 class URLBuildError(ServerError):
@@ -101,6 +105,7 @@ class RequestTimeout(SanicException):
     """
 
     status_code = 408
+    quiet = True
 
 
 class PayloadTooLarge(SanicException):
@@ -109,6 +114,7 @@ class PayloadTooLarge(SanicException):
     """
 
     status_code = 413
+    quiet = True
 
 
 class HeaderNotFound(InvalidUsage):
@@ -117,6 +123,7 @@ class HeaderNotFound(InvalidUsage):
     """
 
     status_code = 400
+    quiet = True
 
 
 class ContentRangeError(SanicException):
@@ -125,6 +132,7 @@ class ContentRangeError(SanicException):
     """
 
     status_code = 416
+    quiet = True
 
     def __init__(self, message, content_range):
         super().__init__(message)
@@ -137,6 +145,7 @@ class HeaderExpectationFailed(SanicException):
     """
 
     status_code = 417
+    quiet = True
 
 
 class Forbidden(SanicException):
@@ -145,6 +154,7 @@ class Forbidden(SanicException):
     """
 
     status_code = 403
+    quiet = True
 
 
 class InvalidRangeType(ContentRangeError):
@@ -153,6 +163,7 @@ class InvalidRangeType(ContentRangeError):
     """
 
     status_code = 416
+    quiet = True
 
 
 class PyFileError(Exception):
@@ -196,6 +207,7 @@ class Unauthorized(SanicException):
     """
 
     status_code = 401
+    quiet = True
 
     def __init__(self, message, status_code=None, scheme=None, **kwargs):
         super().__init__(message, status_code)


Test patch:
diff --git a/tests/test_static.py b/tests/test_static.py
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -471,7 +471,7 @@ def test_stack_trace_on_not_found(app, static_file_directory, caplog):
 
     assert response.status == 404
     assert counter[logging.INFO] == 5
-    assert counter[logging.ERROR] == 1
+    assert counter[logging.ERROR] == 0
 
 
 def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):