Cleanup implementation

This commit is contained in:
Adam Hopkins 2022-09-19 21:34:50 +03:00
parent 8b970dd490
commit 38b4ccf2bc
No known key found for this signature in database
GPG Key ID: 9F85EE6C807303FB
5 changed files with 113 additions and 382 deletions

View File

@ -21,7 +21,6 @@ from functools import partial
from inspect import isawaitable from inspect import isawaitable
from os import environ from os import environ
from socket import socket from socket import socket
from traceback import format_exc
from types import SimpleNamespace from types import SimpleNamespace
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
@ -54,13 +53,8 @@ from sanic.blueprint_group import BlueprintGroup
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
from sanic.config import SANIC_PREFIX, Config from sanic.config import SANIC_PREFIX, Config
from sanic.exceptions import ( from sanic.exceptions import BadRequest, SanicException, URLBuildError
BadRequest, from sanic.handlers import ErrorHandler
SanicException,
ServerError,
URLBuildError,
)
from sanic.handlers import ErrorHandler, RequestManager
from sanic.helpers import _default from sanic.helpers import _default
from sanic.http import Stage from sanic.http import Stage
from sanic.log import ( from sanic.log import (
@ -83,7 +77,7 @@ from sanic.models.futures import (
from sanic.models.handler_types import ListenerType, MiddlewareType from sanic.models.handler_types import ListenerType, MiddlewareType
from sanic.models.handler_types import Sanic as SanicVar from sanic.models.handler_types import Sanic as SanicVar
from sanic.request import Request from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream from sanic.response import BaseHTTPResponse
from sanic.router import Router from sanic.router import Router
from sanic.server.websockets.impl import ConnectionClosed from sanic.server.websockets.impl import ConnectionClosed
from sanic.signals import Signal, SignalRouter from sanic.signals import Signal, SignalRouter
@ -716,284 +710,8 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
): # no cov ): # no cov
raise NotImplementedError raise NotImplementedError
async def _handle_exception(
self,
request: Request,
exception: BaseException,
run_middleware: bool = True,
): # no cov
"""
A handler that catches specific exceptions and outputs a response.
:param request: The current request object
:param exception: The exception that was raised
:raises ServerError: response 500
"""
response = None
await self.dispatch(
"http.lifecycle.exception",
inline=True,
context={"request": request, "exception": exception},
)
if (
request.stream is not None
and request.stream.stage is not Stage.HANDLER
):
error_logger.exception(exception, exc_info=True)
logger.error(
"The error response will not be sent to the client for "
f'the following exception:"{exception}". A previous response '
"has at least partially been sent."
)
handler = self.error_handler._lookup(
exception, request.name if request else None
)
if handler:
logger.warning(
"An error occurred while handling the request after at "
"least some part of the response was sent to the client. "
"The response from your custom exception handler "
f"{handler.__name__} will not be sent to the client."
"Exception handlers should only be used to generate the "
"exception responses. If you would like to perform any "
"other action on a raised exception, consider using a "
"signal handler like "
'`@app.signal("http.lifecycle.exception")`\n'
"For further information, please see the docs: "
"https://sanicframework.org/en/guide/advanced/"
"signals.html",
)
return
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
if run_middleware:
middleware = (
request.route and request.route.extra.request_middleware
) or self.request_middleware
response = await self._run_request_middleware(request, middleware)
# No middleware results
if not response:
try:
response = self.error_handler.response(request, exception)
if isawaitable(response):
response = await response
except Exception as e:
if isinstance(e, SanicException):
response = self.error_handler.default(request, e)
elif self.debug:
response = HTTPResponse(
(
f"Error while handling error: {e}\n"
f"Stack: {format_exc()}"
),
status=500,
)
else:
response = HTTPResponse(
"An error occurred while handling an error", status=500
)
if response is not None:
try:
request.reset_response()
response = await request.respond(response)
except BaseException:
# Skip response middleware
if request.stream:
request.stream.respond(response)
await response.send(end_stream=True)
raise
else:
if request.stream:
response = request.stream.response
# Marked for cleanup and DRY with handle_request/handle_exception
# when ResponseStream is no longer supporder
if isinstance(response, BaseHTTPResponse):
await self.dispatch(
"http.lifecycle.response",
inline=True,
context={
"request": request,
"response": response,
},
)
await response.send(end_stream=True)
elif isinstance(response, ResponseStream):
resp = await response(request)
await self.dispatch(
"http.lifecycle.response",
inline=True,
context={
"request": request,
"response": resp,
},
)
await response.eof()
else:
raise ServerError(
f"Invalid response type {response!r} (need HTTPResponse)"
)
async def handle_request(self, request: Request): # no cov async def handle_request(self, request: Request): # no cov
"""Take a request from the HTTP Server and return a response object raise NotImplementedError
to be sent back The HTTP Server only expects a response object, so
exception handling must be done here
:param request: HTTP Request object
:return: Nothing
"""
async def _handle_request(self, request: Request): # no cov
await self.dispatch(
"http.lifecycle.handle",
inline=True,
context={"request": request},
)
# Define `response` var here to remove warnings about
# allocation before assignment below.
response: Optional[
Union[
BaseHTTPResponse,
Coroutine[Any, Any, Optional[BaseHTTPResponse]],
]
] = None
run_middleware = True
try:
await self.dispatch(
"http.routing.before",
inline=True,
context={"request": request},
)
# Fetch handler from router
route, handler, kwargs = self.router.get(
request.path,
request.method,
request.headers.getone("host", None),
)
request._match_info = {**kwargs}
request.route = route
await self.dispatch(
"http.routing.after",
inline=True,
context={
"request": request,
"route": route,
"kwargs": kwargs,
"handler": handler,
},
)
if (
request.stream
and request.stream.request_body
and not route.ctx.ignore_body
):
if hasattr(handler, "is_stream"):
# Streaming handler: lift the size limit
request.stream.request_max_size = float("inf")
else:
# Non-streaming handler: preload body
await request.receive_body()
# -------------------------------------------- #
# Request Middleware
# -------------------------------------------- #
run_middleware = False
if request.route.extra.request_middleware:
response = await self._run_request_middleware(
request, request.route.extra.request_middleware
)
# No middleware results
if not response:
# -------------------------------------------- #
# Execute Handler
# -------------------------------------------- #
if handler is None:
raise ServerError(
(
"'None' was returned while requesting a "
"handler from the router"
)
)
# Run response handler
await self.dispatch(
"http.handler.before",
inline=True,
context={"request": request},
)
response = handler(request, **request.match_info)
if isawaitable(response):
response = await response
await self.dispatch(
"http.handler.after",
inline=True,
context={"request": request},
)
if request.responded:
if response is not None:
error_logger.error(
"The response object returned by the route handler "
"will not be sent to client. The request has already "
"been responded to."
)
if request.stream is not None:
response = request.stream.response
elif response is not None:
response = await request.respond(response) # type: ignore
elif not hasattr(handler, "is_websocket"):
response = request.stream.response # type: ignore
# Marked for cleanup and DRY with handle_request/handle_exception
# when ResponseStream is no longer supporder
if isinstance(response, BaseHTTPResponse):
await self.dispatch(
"http.lifecycle.response",
inline=True,
context={
"request": request,
"response": response,
},
)
...
await response.send(end_stream=True)
elif isinstance(response, ResponseStream):
resp = await response(request) # type: ignore
await self.dispatch(
"http.lifecycle.response",
inline=True,
context={
"request": request,
"response": resp,
},
)
await response.eof() # type: ignore
else:
if not hasattr(handler, "is_websocket"):
raise ServerError(
f"Invalid response type {response!r} "
"(need HTTPResponse)"
)
except CancelledError:
raise
except Exception as e:
# Response Generation Failed
await self.handle_exception(
request, e, run_middleware=run_middleware
)
async def _websocket_handler( async def _websocket_handler(
self, handler, request, *args, subprotocols=None, **kwargs self, handler, request, *args, subprotocols=None, **kwargs

View File

@ -20,11 +20,12 @@ from sanic.log import deprecation, error_logger, logger
from sanic.models.handler_types import RouteHandler from sanic.models.handler_types import RouteHandler
from sanic.request import Request from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream, text from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream, text
from sanic.touchup import TouchUpMeta
class RequestHandler: class RequestHandler:
def __init__(self, func, request_middleware, response_middleware): def __init__(self, func, request_middleware, response_middleware):
self.func = func self.func = func.func if isinstance(func, RequestHandler) else func
self.request_middleware = request_middleware self.request_middleware = request_middleware
self.response_middleware = response_middleware self.response_middleware = response_middleware
@ -32,7 +33,20 @@ class RequestHandler:
return self.func(*args, **kwargs) return self.func(*args, **kwargs)
class RequestManager: class RequestManager(metaclass=TouchUpMeta):
__touchup__ = (
"cleanup",
"run_request_middleware",
"run_response_middleware",
)
__slots__ = (
"handler",
"request_middleware_run",
"request_middleware",
"request",
"response_middleware_run",
"response_middleware",
)
request: Request request: Request
def __init__(self, request: Request): def __init__(self, request: Request):
@ -76,28 +90,35 @@ class RequestManager:
partial(self.handler, self.request, **self.request.match_info) partial(self.handler, self.request, **self.request.match_info)
) )
async def lifecycle(self, handler): async def lifecycle(self, handler, raise_exception: bool = False):
response: Optional[BaseHTTPResponse] = None response: Optional[BaseHTTPResponse] = None
if not self.request_middleware_run and self.request_middleware: if not self.request_middleware_run and self.request_middleware:
response = await self.run(self.run_request_middleware) response = await self.run(
self.run_request_middleware, raise_exception
)
if not response: if not response:
# Run response handler # Run response handler
response = await self.run(handler) response = await self.run(handler, raise_exception)
if not self.response_middleware_run and self.response_middleware: if not self.response_middleware_run and self.response_middleware:
response = await self.run( response = await self.run(
partial(self.run_response_middleware, response) partial(self.run_response_middleware, response),
raise_exception,
) )
await self.cleanup(response) await self.cleanup(response)
async def run(self, operation) -> Optional[BaseHTTPResponse]: async def run(
self, operation, raise_exception: bool = False
) -> Optional[BaseHTTPResponse]:
try: try:
response = operation() response = operation()
if isawaitable(response): if isawaitable(response):
response = await response response = await response
except Exception as e: except Exception as e:
if raise_exception:
raise
response = await self.error(e) response = await self.error(e)
return response return response
@ -136,12 +157,9 @@ class RequestManager:
try: try:
await self.lifecycle( await self.lifecycle(
partial(error_handler.response, self.request, exception) partial(error_handler.response, self.request, exception), True
) )
except Exception as e: except Exception as e:
await self.lifecycle(
partial(error_handler.default, self.request, e)
)
if isinstance(e, SanicException): if isinstance(e, SanicException):
response = error_handler.default(self.request, e) response = error_handler.default(self.request, e)
elif self.request.app.debug: elif self.request.app.debug:
@ -153,6 +171,7 @@ class RequestManager:
status=500, status=500,
) )
else: else:
error_logger.exception(e)
response = HTTPResponse( response = HTTPResponse(
"An error occurred while handling an error", status=500 "An error occurred while handling an error", status=500
) )
@ -175,30 +194,21 @@ class RequestManager:
elif not hasattr(self.handler, "is_websocket"): elif not hasattr(self.handler, "is_websocket"):
response = self.request.stream.response # type: ignore response = self.request.stream.response # type: ignore
# Marked for cleanup and DRY with handle_request/handle_exception
# when ResponseStream is no longer supporder
if isinstance(response, BaseHTTPResponse): if isinstance(response, BaseHTTPResponse):
# await self.dispatch( await self.request.app.dispatch(
# "http.lifecycle.response", "http.lifecycle.response",
# inline=True, inline=True,
# context={ context={"request": self.request, "response": response},
# "request": self.request, )
# "response": response,
# },
# )
...
await response.send(end_stream=True) await response.send(end_stream=True)
elif isinstance(response, ResponseStream): elif isinstance(response, ResponseStream):
await response(self.request) # type: ignore await response(self.request) # type: ignore
# await self.dispatch(
# "http.lifecycle.response",
# inline=True,
# context={
# "request": self.request,
# "response": resp,
# },
# )
await response.eof() # type: ignore await response.eof() # type: ignore
await self.request.app.dispatch(
"http.lifecycle.response",
inline=True,
context={"request": self.request, "response": response},
)
else: else:
if not hasattr(self.handler, "is_websocket"): if not hasattr(self.handler, "is_websocket"):
raise ServerError( raise ServerError(
@ -219,27 +229,27 @@ class RequestManager:
self.request_middleware_run = True self.request_middleware_run = True
for middleware in self.request_middleware: for middleware in self.request_middleware:
# await self.dispatch( await self.request.app.dispatch(
# "http.middleware.before", "http.middleware.before",
# inline=True, inline=True,
# context={ context={"request": self.request, "response": None},
# "request": request, condition={"attach_to": "request"},
# "response": None, )
# },
# condition={"attach_to": "request"},
# )
try:
response = await self.run(partial(middleware, self.request)) response = await self.run(partial(middleware, self.request))
except Exception:
error_logger.exception(
"Exception occurred in one of request middleware handlers"
)
raise
# await self.dispatch( await self.request.app.dispatch(
# "http.middleware.after", "http.middleware.after",
# inline=True, inline=True,
# context={ context={"request": self.request, "response": None},
# "request": request, condition={"attach_to": "request"},
# "response": None, )
# },
# condition={"attach_to": "request"},
# )
if response: if response:
return response return response
@ -250,46 +260,34 @@ class RequestManager:
) -> BaseHTTPResponse: ) -> BaseHTTPResponse:
self.response_middleware_run = True self.response_middleware_run = True
for middleware in self.response_middleware: for middleware in self.response_middleware:
# await self.dispatch( await self.request.app.dispatch(
# "http.middleware.before", "http.middleware.before",
# inline=True, inline=True,
# context={ context={"request": self.request, "response": None},
# "request": request, condition={"attach_to": "request"},
# "response": None, )
# },
# condition={"attach_to": "request"},
# )
resp = await self.run(partial(middleware, self.request, response)) try:
resp = await self.run(
partial(middleware, self.request, response), True
)
except Exception as e:
error_logger.exception(
"Exception occurred in one of response middleware handlers"
)
await self.error(e)
resp = None
# await self.dispatch( await self.request.app.dispatch(
# "http.middleware.after", "http.middleware.after",
# inline=True, inline=True,
# context={ context={"request": self.request, "response": None},
# "request": request, condition={"attach_to": "request"},
# "response": None, )
# },
# condition={"attach_to": "request"},
# )
if resp: if resp:
return resp return resp
return response return response
# try:
# middleware = (
# self.route and self.route.extra.response_middleware
# ) or self.app.response_middleware
# if middleware:
# response = await self.app._run_response_middleware(
# self, response, middleware
# )
# except CancelledErrors:
# raise
# except Exception:
# error_logger.exception(
# "Exception occurred in one of response middleware handlers"
# )
# return None
def resolve_route(self) -> Route: def resolve_route(self) -> Route:
# Fetch handler from router # Fetch handler from router
@ -303,11 +301,11 @@ class RequestManager:
self.request.route = route self.request.route = route
self.handler = handler self.handler = handler
if route.handler and route.handler.request_middleware: if handler and handler.request_middleware:
self.request_middleware = route.handler.request_middleware self.request_middleware = handler.request_middleware
if route.handler and route.handler.response_middleware: if handler and handler.response_middleware:
self.response_middleware = route.handler.response_middleware self.response_middleware = handler.response_middleware
return route return route

View File

@ -14,7 +14,7 @@ class MiddlewareLocation(IntEnum):
class Middleware: class Middleware:
counter = count() _counter = count()
__slots__ = ("func", "priority", "location", "definition") __slots__ = ("func", "priority", "location", "definition")
@ -27,7 +27,7 @@ class Middleware:
self.func = func self.func = func
self.priority = priority self.priority = priority
self.location = location self.location = location
self.definition = next(Middleware.counter) self.definition = next(Middleware._counter)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs) return self.func(*args, **kwargs)
@ -60,3 +60,7 @@ class Middleware:
for middleware in collection for middleware in collection
] ]
) )
@classmethod
def reset_count(cls):
cls._counter = count()

View File

@ -13,6 +13,7 @@ from sanic_routing.route import Route
from sanic.constants import HTTP_METHODS from sanic.constants import HTTP_METHODS
from sanic.errorpages import check_error_format from sanic.errorpages import check_error_format
from sanic.exceptions import MethodNotAllowed, NotFound, SanicException from sanic.exceptions import MethodNotAllowed, NotFound, SanicException
from sanic.handlers import RequestHandler
from sanic.models.handler_types import RouteHandler from sanic.models.handler_types import RouteHandler
@ -31,9 +32,11 @@ class Router(BaseRouter):
def _get( def _get(
self, path: str, method: str, host: Optional[str] self, path: str, method: str, host: Optional[str]
) -> Tuple[Route, RouteHandler, Dict[str, Any]]: ) -> Tuple[Route, RequestHandler, Dict[str, Any]]:
try: try:
return self.resolve( # We know this will always be RequestHandler, so we can ignore
# typing issue here
return self.resolve( # type: ignore
path=path, path=path,
method=method, method=method,
extra={"host": host} if host else None, extra={"host": host} if host else None,
@ -50,7 +53,7 @@ class Router(BaseRouter):
@lru_cache(maxsize=ROUTER_CACHE_SIZE) @lru_cache(maxsize=ROUTER_CACHE_SIZE)
def get( # type: ignore def get( # type: ignore
self, path: str, method: str, host: Optional[str] self, path: str, method: str, host: Optional[str]
) -> Tuple[Route, RouteHandler, Dict[str, Any]]: ) -> Tuple[Route, RequestHandler, Dict[str, Any]]:
""" """
Retrieve a `Route` object containing the details about how to handle Retrieve a `Route` object containing the details about how to handle
a response for a given request a response for a given request
@ -59,7 +62,7 @@ class Router(BaseRouter):
:type request: Request :type request: Request
:return: details needed for handling the request and returning the :return: details needed for handling the request and returning the
correct response correct response
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]] :rtype: Tuple[ Route, RequestHandler, Dict[str, Any]]
""" """
return self._get(path, method, host) return self._get(path, method, host)
@ -114,7 +117,7 @@ class Router(BaseRouter):
params = dict( params = dict(
path=uri, path=uri,
handler=handler, handler=RequestHandler(handler, [], []),
methods=frozenset(map(str, methods)) if methods else None, methods=frozenset(map(str, methods)) if methods else None,
name=name, name=name,
strict=strict_slashes, strict=strict_slashes,

View File

@ -4,12 +4,20 @@ from asyncio import CancelledError
from itertools import count from itertools import count
from unittest.mock import Mock from unittest.mock import Mock
import pytest
from sanic.exceptions import NotFound from sanic.exceptions import NotFound
from sanic.middleware import Middleware, MiddlewareLocation from sanic.middleware import Middleware
from sanic.request import Request from sanic.request import Request
from sanic.response import HTTPResponse, json, text from sanic.response import HTTPResponse, json, text
@pytest.fixture(autouse=True)
def reset_middleware():
yield
Middleware.reset_count()
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
# GET # GET
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@ -185,7 +193,7 @@ def test_middleware_response_raise_exception(app, caplog):
with caplog.at_level(logging.ERROR): with caplog.at_level(logging.ERROR):
reqrequest, response = app.test_client.get("/fail") reqrequest, response = app.test_client.get("/fail")
assert response.status == 404 assert response.status == 500
# 404 errors are not logged # 404 errors are not logged
assert ( assert (
"sanic.error", "sanic.error",