Skip to content

Commit

Permalink
Add a new exception signal for ALL exceptions raised anywhere in appl…
Browse files Browse the repository at this point in the history
…ication (#2724)
  • Loading branch information
ahopkins committed Jul 9, 2023
1 parent 11a0b15 commit 976da69
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 4 deletions.
4 changes: 4 additions & 0 deletions sanic/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,10 @@ async def handle_exception(
:raises ServerError: response 500
"""
response = None
await self.dispatch(
"server.lifecycle.exception",
context={"exception": exception},
)
await self.dispatch(
"http.lifecycle.exception",
inline=True,
Expand Down
4 changes: 3 additions & 1 deletion sanic/handlers/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from sanic.exceptions import ServerError
from sanic.log import error_logger
from sanic.models.handler_types import RouteHandler
from sanic.request.types import Request
from sanic.response import text
from sanic.response.types import HTTPResponse


class ErrorHandler:
Expand Down Expand Up @@ -148,7 +150,7 @@ def response(self, request, exception):
return text("An error occurred while handling an error", 500)
return response

def default(self, request, exception):
def default(self, request: Request, exception: Exception) -> HTTPResponse:
"""
Provide a default behavior for the objects of :class:`ErrorHandler`.
If a developer chooses to extent the :class:`ErrorHandler` they can
Expand Down
12 changes: 12 additions & 0 deletions sanic/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Event(Enum):
SERVER_INIT_BEFORE = "server.init.before"
SERVER_SHUTDOWN_AFTER = "server.shutdown.after"
SERVER_SHUTDOWN_BEFORE = "server.shutdown.before"
SERVER_LIFECYCLE_EXCEPTION = "server.lifecycle.exception"
HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin"
HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete"
HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception"
Expand All @@ -43,6 +44,7 @@ class Event(Enum):
Event.SERVER_INIT_BEFORE.value,
Event.SERVER_SHUTDOWN_AFTER.value,
Event.SERVER_SHUTDOWN_BEFORE.value,
Event.SERVER_LIFECYCLE_EXCEPTION.value,
),
"http": (
Event.HTTP_LIFECYCLE_BEGIN.value,
Expand Down Expand Up @@ -168,6 +170,16 @@ async def _dispatch(
elif maybe_coroutine:
return maybe_coroutine
return None
except Exception as e:
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:
error_logger.exception(e)

if event != Event.SERVER_LIFECYCLE_EXCEPTION.value:
await self.dispatch(
Event.SERVER_LIFECYCLE_EXCEPTION.value,
context={"exception": e},
)
raise e
finally:
for signal_event in events:
signal_event.clear()
Expand Down
30 changes: 27 additions & 3 deletions tests/test_signal_handlers.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import asyncio
import os
import signal

from queue import Queue
from types import SimpleNamespace
from typing import Optional
from unittest.mock import MagicMock

import pytest

from sanic_testing.testing import HOST, PORT

from sanic import Sanic
from sanic.compat import ctrlc_workaround_for_windows
from sanic.exceptions import BadRequest
from sanic.exceptions import BadRequest, ServerError
from sanic.response import HTTPResponse
from sanic.signals import Event


async def stop(app, loop):
Expand Down Expand Up @@ -148,3 +149,26 @@ async def hello_route(request):
BadRequest, match="Invalid event registration: Missing event name"
):
app.listener(stop)


def test_signal_server_lifecycle_exception(app: Sanic):
trigger: Optional[Exception] = None

@app.route("/hello")
async def hello_route(request):
return HTTPResponse()

@app.signal(Event.SERVER_LIFECYCLE_EXCEPTION)
async def test_signal(exception: Exception):
nonlocal trigger
trigger = exception

@app.before_server_start
async def test_before_server_start(app):
raise ServerError("test_before_server_start")

with pytest.raises(ServerError, match="test_before_server_start"):
app.run(single_process=True)

assert isinstance(trigger, ServerError)
assert str(trigger) == "test_before_server_start"

0 comments on commit 976da69

Please sign in to comment.