ASGI lifespan failure on exception (#2627)

This commit is contained in:
Adam Hopkins 2022-12-16 08:56:07 +02:00 committed by GitHub
parent 95ee518aec
commit a3ff0c13b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 62 additions and 6 deletions

View File

@ -9,7 +9,7 @@ from sanic.compat import Header
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.helpers import Default from sanic.helpers import Default
from sanic.http import Stage 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.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
from sanic.request import Request from sanic.request import Request
from sanic.response import BaseHTTPResponse from sanic.response import BaseHTTPResponse
@ -85,12 +85,26 @@ class Lifespan:
) -> None: ) -> None:
message = await receive() message = await receive()
if message["type"] == "lifespan.startup": if message["type"] == "lifespan.startup":
try:
await self.startup() 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"}) await send({"type": "lifespan.startup.complete"})
message = await receive() message = await receive()
if message["type"] == "lifespan.shutdown": if message["type"] == "lifespan.shutdown":
try:
await self.shutdown() 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"}) await send({"type": "lifespan.shutdown.complete"})

View File

@ -8,7 +8,7 @@ import uvicorn
from sanic import Sanic from sanic import Sanic
from sanic.application.state import Mode 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.exceptions import BadRequest, Forbidden, ServiceUnavailable
from sanic.request import Request from sanic.request import Request
from sanic.response import json, text from sanic.response import json, text
@ -16,6 +16,12 @@ from sanic.server.websockets.connection import WebSocketConnection
from sanic.signals import RESERVED_NAMESPACES from sanic.signals import RESERVED_NAMESPACES
try:
from unittest.mock import AsyncMock
except ImportError:
from tests.asyncmock import AsyncMock # type: ignore
@pytest.fixture @pytest.fixture
def message_stack(): def message_stack():
return deque() return deque()
@ -558,3 +564,39 @@ async def test_asgi_serve_location(app):
_, response = await app.asgi_client.get("/") _, response = await app.asgi_client.get("/")
assert response.text == "http://<ASGI>" 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"}
)