Repository: sanic-org/sanic

Issue:
Uvicorn ignores listeners errors
**Describe the bug**
When an exception happens on a listener the error is not reported and the server doesn't crash when using Uvicorn.


**Code snippet**
```python
from sanic import Sanic
from sanic.response import text

app = Sanic("MyHelloWorldApp")

@app.listener("before_server_start")
async def raises(app, loop):
    print("Trying to run faster")
    raise Exception("oh noes")

@app.get("/")
async def hello_world(request):
    return text("Hello, world.")
```

```
~ uvicorn app:app
INFO:     Started server process [49837]
INFO:     Waiting for application startup.
[2022-01-25 14:57:34 +0100] [49837] [INFO]
  ┌────────────────────────────────────────────────────────────────┐
  │                         Sanic v21.12.1                         │
  │                                                                │
  ├───────────────────────┬────────────────────────────────────────┤
  │                       │     mode: production, ASGI             │
  │     ▄███ █████ ██     │   server: sanic                        │
  │    ██                 │   python: 3.9.10                       │
  │     ▀███████ ███▄     │ platform: macOS-12.1-x86_64-i386-64bit │
  │                 ██    │ packages: sanic-routing==0.7.2         │
  │    ████ ████████▀     │                                        │
  │                       │                                        │
  │ Build Fast. Run Fast. │                                        │
  └───────────────────────┴────────────────────────────────────────┘

/Users/andre.ericson/projects/sanic-uvicorn/.venv/lib/python3.9/site-packages/sanic/asgi.py:27: UserWarning: You have set a listener for "before_server_start" in ASGI mode. It will be executed as early as possible, but not before the ASGI server is started.
  warnings.warn(
Trying to run faster
INFO:     ASGI 'lifespan' protocol appears unsupported.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
``` 
**Expected behavior**
The server should crash, or at least log an error.


**Environment (please complete the following information):**
 - MacOS
 - python 3.9.10
 ```
 pip list
Package       Version
------------- ------------
aiofiles      0.8.0
asgiref       3.5.0
click         8.0.3
h11           0.13.0
httptools     0.3.0
multidict     5.2.0
pip           21.3.1
sanic         21.12.1
sanic-routing 0.7.2
setuptools    58.1.0
ujson         5.1.0
uvicorn       0.17.0.post1
uvloop        0.16.0
websockets    10.1
 ```


**Additional context**
Works as expected with `sanic app.app` and also with GunicornWorker.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2627?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) Report
Base: **88.548**% // Head: **88.532**% // Decreases project coverage by **`-0.016%`** :warning:
> Coverage data is based on head [(`060a195`)](https://codecov.io/gh/sanic-org/sanic/pull/2627?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) compared to base [(`71d3d87`)](https://codecov.io/gh/sanic-org/sanic/commit/71d3d87bccf70a01e2d54f081c8c131d89d90130?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).
> Patch coverage: 100.000% of modified lines in pull request are covered.

> :exclamation: Current head 060a195 differs from pull request most recent head f9358ef. Consider uploading reports for the commit f9358ef to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@              Coverage Diff              @@
##              main     #2627       +/-   ##
=============================================
- Coverage   88.548%   88.532%   -0.017%     
=============================================
  Files           81        81               
  Lines         6698      6706        +8     
  Branches      1143      1143               
=============================================
+ Hits          5931      5937        +6     
- Misses         529       531        +2     
  Partials       238       238               
```


| [Impacted Files](https://codecov.io/gh/sanic-org/sanic/pull/2627?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) | Coverage Δ | |
|---|---|---|
| [sanic/asgi.py](https://codecov.io/gh/sanic-org/sanic/pull/2627/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvYXNnaS5weQ==) | `92.307% <100.000%> (+2.143%)` | :arrow_up: |
| [sanic/server/websockets/impl.py](https://codecov.io/gh/sanic-org/sanic/pull/2627/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvc2VydmVyL3dlYnNvY2tldHMvaW1wbC5weQ==) | `37.557% <0.000%> (-0.922%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2627?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).


Patch:
diff --git a/sanic/asgi.py b/sanic/asgi.py
index c3d669e7..d8ab4cfa 100644
--- a/sanic/asgi.py
+++ b/sanic/asgi.py
@@ -9,7 +9,7 @@ from sanic.compat import Header
 from sanic.exceptions import ServerError
 from sanic.helpers import Default
 from sanic.http import Stage
-from sanic.log import logger
+from sanic.log import error_logger, logger
 from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
 from sanic.request import Request
 from sanic.response import BaseHTTPResponse
@@ -85,13 +85,27 @@ class Lifespan:
     ) -> None:
         message = await receive()
         if message["type"] == "lifespan.startup":
-            await self.startup()
-            await send({"type": "lifespan.startup.complete"})
+            try:
+                await self.startup()
+            except Exception as e:
+                error_logger.exception(e)
+                await send(
+                    {"type": "lifespan.startup.failed", "message": str(e)}
+                )
+            else:
+                await send({"type": "lifespan.startup.complete"})
 
         message = await receive()
         if message["type"] == "lifespan.shutdown":
-            await self.shutdown()
-            await send({"type": "lifespan.shutdown.complete"})
+            try:
+                await self.shutdown()
+            except Exception as e:
+                error_logger.exception(e)
+                await send(
+                    {"type": "lifespan.shutdown.failed", "message": str(e)}
+                )
+            else:
+                await send({"type": "lifespan.shutdown.complete"})
 
 
 class ASGIApp:


Test patch:
diff --git a/tests/test_asgi.py b/tests/test_asgi.py
index e612e3fe..0c76a67f 100644
--- a/tests/test_asgi.py
+++ b/tests/test_asgi.py
@@ -8,7 +8,7 @@ import uvicorn
 
 from sanic import Sanic
 from sanic.application.state import Mode
-from sanic.asgi import MockTransport
+from sanic.asgi import ASGIApp, MockTransport
 from sanic.exceptions import BadRequest, Forbidden, ServiceUnavailable
 from sanic.request import Request
 from sanic.response import json, text
@@ -16,6 +16,12 @@ from sanic.server.websockets.connection import WebSocketConnection
 from sanic.signals import RESERVED_NAMESPACES
 
 
+try:
+    from unittest.mock import AsyncMock
+except ImportError:
+    from tests.asyncmock import AsyncMock  # type: ignore
+
+
 @pytest.fixture
 def message_stack():
     return deque()
@@ -558,3 +564,39 @@ async def test_asgi_serve_location(app):
 
     _, response = await app.asgi_client.get("/")
     assert response.text == "http://<ASGI>"
+
+
+@pytest.mark.asyncio
+async def test_error_on_lifespan_exception_start(app, caplog):
+    @app.before_server_start
+    async def before_server_start(_):
+        1 / 0
+
+    recv = AsyncMock(return_value={"type": "lifespan.startup"})
+    send = AsyncMock()
+    app.asgi = True
+
+    with caplog.at_level(logging.ERROR):
+        await ASGIApp.create(app, {"type": "lifespan"}, recv, send)
+
+    send.assert_awaited_once_with(
+        {"type": "lifespan.startup.failed", "message": "division by zero"}
+    )
+
+
+@pytest.mark.asyncio
+async def test_error_on_lifespan_exception_stop(app: Sanic):
+    @app.before_server_stop
+    async def before_server_stop(_):
+        1 / 0
+
+    recv = AsyncMock(return_value={"type": "lifespan.shutdown"})
+    send = AsyncMock()
+    app.asgi = True
+    await app._startup()
+
+    await ASGIApp.create(app, {"type": "lifespan"}, recv, send)
+
+    send.assert_awaited_once_with(
+        {"type": "lifespan.shutdown.failed", "message": "division by zero"}
+    )