Add a new exception signal for ALL exceptions raised anywhere in application (#2724)

This commit is contained in:
Adam Hopkins 2023-07-09 10:53:36 +03:00 committed by GitHub
parent 11a0b15194
commit 976da69e79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 46 additions and 4 deletions

View File

@ -773,6 +773,10 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
:raises ServerError: response 500 :raises ServerError: response 500
""" """
response = None response = None
await self.dispatch(
"server.lifecycle.exception",
context={"exception": exception},
)
await self.dispatch( await self.dispatch(
"http.lifecycle.exception", "http.lifecycle.exception",
inline=True, inline=True,

View File

@ -6,7 +6,9 @@ from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.log import error_logger from sanic.log import error_logger
from sanic.models.handler_types import RouteHandler from sanic.models.handler_types import RouteHandler
from sanic.request.types import Request
from sanic.response import text from sanic.response import text
from sanic.response.types import HTTPResponse
class ErrorHandler: class ErrorHandler:
@ -148,7 +150,7 @@ class ErrorHandler:
return text("An error occurred while handling an error", 500) return text("An error occurred while handling an error", 500)
return response 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`. Provide a default behavior for the objects of :class:`ErrorHandler`.
If a developer chooses to extent the :class:`ErrorHandler` they can If a developer chooses to extent the :class:`ErrorHandler` they can

View File

@ -20,6 +20,7 @@ class Event(Enum):
SERVER_INIT_BEFORE = "server.init.before" SERVER_INIT_BEFORE = "server.init.before"
SERVER_SHUTDOWN_AFTER = "server.shutdown.after" SERVER_SHUTDOWN_AFTER = "server.shutdown.after"
SERVER_SHUTDOWN_BEFORE = "server.shutdown.before" SERVER_SHUTDOWN_BEFORE = "server.shutdown.before"
SERVER_LIFECYCLE_EXCEPTION = "server.lifecycle.exception"
HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin" HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin"
HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete" HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete"
HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception" HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception"
@ -43,6 +44,7 @@ RESERVED_NAMESPACES = {
Event.SERVER_INIT_BEFORE.value, Event.SERVER_INIT_BEFORE.value,
Event.SERVER_SHUTDOWN_AFTER.value, Event.SERVER_SHUTDOWN_AFTER.value,
Event.SERVER_SHUTDOWN_BEFORE.value, Event.SERVER_SHUTDOWN_BEFORE.value,
Event.SERVER_LIFECYCLE_EXCEPTION.value,
), ),
"http": ( "http": (
Event.HTTP_LIFECYCLE_BEGIN.value, Event.HTTP_LIFECYCLE_BEGIN.value,
@ -168,6 +170,16 @@ class SignalRouter(BaseRouter):
elif maybe_coroutine: elif maybe_coroutine:
return maybe_coroutine return maybe_coroutine
return None 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: finally:
for signal_event in events: for signal_event in events:
signal_event.clear() signal_event.clear()

View File

@ -1,18 +1,19 @@
import asyncio import asyncio
import os import os
import signal import signal
from queue import Queue from queue import Queue
from types import SimpleNamespace from types import SimpleNamespace
from typing import Optional
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from sanic_testing.testing import HOST, PORT from sanic_testing.testing import HOST, PORT
from sanic import Sanic
from sanic.compat import ctrlc_workaround_for_windows 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.response import HTTPResponse
from sanic.signals import Event
async def stop(app, loop): async def stop(app, loop):
@ -148,3 +149,26 @@ def test_signals_with_invalid_invocation(app):
BadRequest, match="Invalid event registration: Missing event name" BadRequest, match="Invalid event registration: Missing event name"
): ):
app.listener(stop) 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"