Skip to content

Commit

Permalink
ASGI lifespan failure on exception (#2627)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins committed Dec 16, 2022
1 parent 95ee518 commit a3ff0c1
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 6 deletions.
24 changes: 19 additions & 5 deletions sanic/asgi.py
Expand Up @@ -9,7 +9,7 @@
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
Expand Down Expand Up @@ -85,13 +85,27 @@ async def __call__(
) -> 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:
Expand Down
44 changes: 43 additions & 1 deletion tests/test_asgi.py
Expand Up @@ -8,14 +8,20 @@

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
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()
Expand Down Expand Up @@ -558,3 +564,39 @@ def _request(request: Request):

_, 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"}
)

0 comments on commit a3ff0c1

Please sign in to comment.