WIP
This commit is contained in:
parent
beb5c62767
commit
c7bac72137
12
sanic/app.py
12
sanic/app.py
|
@ -60,7 +60,7 @@ from sanic.exceptions import (
|
|||
ServerError,
|
||||
URLBuildError,
|
||||
)
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic.handlers import ErrorHandler, RequestManager
|
||||
from sanic.helpers import _default
|
||||
from sanic.http import Stage
|
||||
from sanic.log import (
|
||||
|
@ -705,6 +705,14 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||
request: Request,
|
||||
exception: BaseException,
|
||||
run_middleware: bool = True,
|
||||
): # no cov
|
||||
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.
|
||||
|
@ -830,6 +838,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||
:param request: HTTP Request object
|
||||
:return: Nothing
|
||||
"""
|
||||
|
||||
async def _handle_request(self, request: Request): # no cov
|
||||
await self.dispatch(
|
||||
"http.lifecycle.handle",
|
||||
inline=True,
|
||||
|
|
|
@ -7,6 +7,7 @@ from urllib.parse import quote
|
|||
|
||||
from sanic.compat import Header
|
||||
from sanic.exceptions import ServerError
|
||||
from sanic.handlers import RequestManager
|
||||
from sanic.helpers import _default
|
||||
from sanic.http import Stage
|
||||
from sanic.log import logger
|
||||
|
@ -230,8 +231,9 @@ class ASGIApp:
|
|||
"""
|
||||
Handle the incoming request.
|
||||
"""
|
||||
manager = RequestManager.create(self.request)
|
||||
try:
|
||||
self.stage = Stage.HANDLER
|
||||
await self.sanic_app.handle_request(self.request)
|
||||
await manager.handle()
|
||||
except Exception as e:
|
||||
await self.sanic_app.handle_exception(self.request, e)
|
||||
await manager.error(e)
|
||||
|
|
|
@ -1,16 +1,319 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from traceback import format_exc
|
||||
from typing import Dict, List, Optional, Tuple, Type
|
||||
|
||||
from sanic_routing import Route
|
||||
|
||||
from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
|
||||
from sanic.exceptions import (
|
||||
HeaderNotFound,
|
||||
InvalidRangeType,
|
||||
RangeNotSatisfiable,
|
||||
SanicException,
|
||||
ServerError,
|
||||
)
|
||||
from sanic.log import deprecation, error_logger
|
||||
from sanic.http.constants import Stage
|
||||
from sanic.log import deprecation, error_logger, logger
|
||||
from sanic.models.handler_types import RouteHandler
|
||||
from sanic.response import text
|
||||
from sanic.request import Request
|
||||
from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream, text
|
||||
|
||||
|
||||
class RequestHandler:
|
||||
def __init__(self, func, request_middleware, response_middleware):
|
||||
self.func = func
|
||||
self.request_middleware = request_middleware
|
||||
self.response_middleware = response_middleware
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.func(*args, **kwargs)
|
||||
|
||||
|
||||
class RequestManager:
|
||||
request: Request
|
||||
|
||||
def __init__(self, request: Request):
|
||||
self.request_middleware_run = False
|
||||
self.response_middleware_run = False
|
||||
self.handler = self._noop
|
||||
self.set_request(request)
|
||||
|
||||
@classmethod
|
||||
def create(cls, request: Request) -> RequestManager:
|
||||
return cls(request)
|
||||
|
||||
def set_request(self, request: Request):
|
||||
request._manager = self
|
||||
self.request = request
|
||||
self.request_middleware = request.app.request_middleware
|
||||
self.response_middleware = request.app.response_middleware
|
||||
|
||||
async def handle(self):
|
||||
route = self.resolve_route()
|
||||
|
||||
if self.handler is None:
|
||||
await self.error(
|
||||
ServerError(
|
||||
(
|
||||
"'None' was returned while requesting a "
|
||||
"handler from the router"
|
||||
)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if (
|
||||
self.request.stream
|
||||
and self.request.stream.request_body
|
||||
and not route.ctx.ignore_body
|
||||
):
|
||||
await self.receive_body()
|
||||
|
||||
await self.lifecycle(
|
||||
partial(self.handler, self.request, **self.request.match_info)
|
||||
)
|
||||
|
||||
async def lifecycle(self, handler):
|
||||
response: Optional[BaseHTTPResponse] = None
|
||||
if not self.request_middleware_run and self.request_middleware:
|
||||
response = await self.run(self.run_request_middleware)
|
||||
|
||||
if not response:
|
||||
# Run response handler
|
||||
response = await self.run(handler)
|
||||
|
||||
if not self.response_middleware_run and self.response_middleware:
|
||||
response = await self.run(
|
||||
partial(self.run_response_middleware, response)
|
||||
)
|
||||
|
||||
await self.cleanup(response)
|
||||
|
||||
async def run(self, operation) -> Optional[BaseHTTPResponse]:
|
||||
try:
|
||||
response = operation()
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
except Exception as e:
|
||||
response = await self.error(e)
|
||||
return response
|
||||
|
||||
async def error(self, exception: Exception):
|
||||
error_handler = self.request.app.error_handler
|
||||
if (
|
||||
self.request.stream is not None
|
||||
and self.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 = error_handler._lookup(
|
||||
exception, self.request.name if self.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
|
||||
|
||||
try:
|
||||
await self.lifecycle(
|
||||
partial(error_handler.response, self.request, exception)
|
||||
)
|
||||
except Exception as e:
|
||||
await self.lifecycle(
|
||||
partial(error_handler.default, self.request, e)
|
||||
)
|
||||
if isinstance(e, SanicException):
|
||||
response = error_handler.default(self.request, e)
|
||||
elif self.request.app.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
|
||||
)
|
||||
return response
|
||||
return None
|
||||
|
||||
async def cleanup(self, response: Optional[BaseHTTPResponse]):
|
||||
if self.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 self.request.stream is not None:
|
||||
response = self.request.stream.response
|
||||
elif response is not None:
|
||||
self.request.reset_response()
|
||||
response = await self.request.respond(response) # type: ignore
|
||||
elif not hasattr(self.handler, "is_websocket"):
|
||||
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):
|
||||
# await self.dispatch(
|
||||
# "http.lifecycle.response",
|
||||
# inline=True,
|
||||
# context={
|
||||
# "request": self.request,
|
||||
# "response": response,
|
||||
# },
|
||||
# )
|
||||
...
|
||||
await response.send(end_stream=True)
|
||||
elif isinstance(response, ResponseStream):
|
||||
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
|
||||
else:
|
||||
if not hasattr(self.handler, "is_websocket"):
|
||||
raise ServerError(
|
||||
f"Invalid response type {response!r} "
|
||||
"(need HTTPResponse)"
|
||||
)
|
||||
|
||||
async def receive_body(self):
|
||||
if hasattr(self.handler, "is_stream"):
|
||||
# Streaming handler: lift the size limit
|
||||
self.request.stream.request_max_size = float("inf")
|
||||
else:
|
||||
# Non-streaming handler: preload body
|
||||
await self.request.receive_body()
|
||||
|
||||
async def run_request_middleware(self) -> Optional[BaseHTTPResponse]:
|
||||
self.request._request_middleware_started = True
|
||||
self.request_middleware_run = True
|
||||
|
||||
for middleware in self.request_middleware:
|
||||
# await self.dispatch(
|
||||
# "http.middleware.before",
|
||||
# inline=True,
|
||||
# context={
|
||||
# "request": request,
|
||||
# "response": None,
|
||||
# },
|
||||
# condition={"attach_to": "request"},
|
||||
# )
|
||||
|
||||
response = await self.run(partial(middleware, self.request))
|
||||
|
||||
# await self.dispatch(
|
||||
# "http.middleware.after",
|
||||
# inline=True,
|
||||
# context={
|
||||
# "request": request,
|
||||
# "response": None,
|
||||
# },
|
||||
# condition={"attach_to": "request"},
|
||||
# )
|
||||
|
||||
if response:
|
||||
return response
|
||||
return None
|
||||
|
||||
async def run_response_middleware(
|
||||
self, response: BaseHTTPResponse
|
||||
) -> BaseHTTPResponse:
|
||||
self.response_middleware_run = True
|
||||
for middleware in self.response_middleware:
|
||||
# await self.dispatch(
|
||||
# "http.middleware.before",
|
||||
# inline=True,
|
||||
# context={
|
||||
# "request": request,
|
||||
# "response": None,
|
||||
# },
|
||||
# condition={"attach_to": "request"},
|
||||
# )
|
||||
|
||||
resp = await self.run(partial(middleware, self.request, response))
|
||||
|
||||
# await self.dispatch(
|
||||
# "http.middleware.after",
|
||||
# inline=True,
|
||||
# context={
|
||||
# "request": request,
|
||||
# "response": None,
|
||||
# },
|
||||
# condition={"attach_to": "request"},
|
||||
# )
|
||||
|
||||
if resp:
|
||||
return resp
|
||||
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:
|
||||
# Fetch handler from router
|
||||
route, handler, kwargs = self.request.app.router.get(
|
||||
self.request.path,
|
||||
self.request.method,
|
||||
self.request.headers.getone("host", None),
|
||||
)
|
||||
|
||||
self.request._match_info = {**kwargs}
|
||||
self.request.route = route
|
||||
self.handler = handler
|
||||
|
||||
if route.handler and route.handler.request_middleware:
|
||||
self.request_middleware = route.handler.request_middleware
|
||||
|
||||
if route.handler and route.handler.response_middleware:
|
||||
self.response_middleware = route.handler.response_middleware
|
||||
|
||||
return route
|
||||
|
||||
@staticmethod
|
||||
def _noop(_):
|
||||
...
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
|
|
|
@ -124,7 +124,8 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||
|
||||
self.stage = Stage.HANDLER
|
||||
self.request.conn_info = self.protocol.conn_info
|
||||
await self.protocol.request_handler(self.request)
|
||||
|
||||
await self.request.manager.handle()
|
||||
|
||||
# Handler finished, response should've been sent
|
||||
if self.stage is Stage.HANDLER and not self.upgrade_websocket:
|
||||
|
@ -246,6 +247,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||
transport=self.protocol.transport,
|
||||
app=self.protocol.app,
|
||||
)
|
||||
self.protocol.request_handler.create(request)
|
||||
self.protocol.request_class._current.set(request)
|
||||
await self.dispatch(
|
||||
"http.lifecycle.request",
|
||||
|
@ -419,12 +421,11 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||
|
||||
# From request and handler states we can respond, otherwise be silent
|
||||
if self.stage is Stage.HANDLER:
|
||||
app = self.protocol.app
|
||||
|
||||
if self.request is None:
|
||||
self.create_empty_request()
|
||||
self.protocol.request_handler.create(self.request)
|
||||
|
||||
await app.handle_exception(self.request, exception)
|
||||
await self.request.manager.error(exception)
|
||||
|
||||
def create_empty_request(self) -> None:
|
||||
"""
|
||||
|
|
|
@ -33,9 +33,10 @@ class Middleware:
|
|||
return self.func(*args, **kwargs)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
name = getattr(self.func, "__name__", str(self.func))
|
||||
return (
|
||||
f"{self.__class__.__name__}("
|
||||
f"func=<function {self.func.__name__}>, "
|
||||
f"func=<function {name}>, "
|
||||
f"priority={self.priority}, "
|
||||
f"location={self.location.name})"
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ from operator import attrgetter
|
|||
from typing import List
|
||||
|
||||
from sanic.base.meta import SanicMeta
|
||||
from sanic.handlers import RequestHandler
|
||||
from sanic.middleware import Middleware, MiddlewareLocation
|
||||
from sanic.models.futures import FutureMiddleware
|
||||
from sanic.router import Router
|
||||
|
@ -104,19 +105,23 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
|||
self.named_response_middleware.get(route.name, deque()),
|
||||
location=MiddlewareLocation.RESPONSE,
|
||||
)
|
||||
route.extra.request_middleware = deque(
|
||||
|
||||
route.handler = RequestHandler(
|
||||
route.handler,
|
||||
deque(
|
||||
sorted(
|
||||
request_middleware,
|
||||
key=attrgetter("order"),
|
||||
reverse=True,
|
||||
)
|
||||
)
|
||||
route.extra.response_middleware = deque(
|
||||
),
|
||||
deque(
|
||||
sorted(
|
||||
response_middleware,
|
||||
key=attrgetter("order"),
|
||||
reverse=True,
|
||||
)[::-1]
|
||||
),
|
||||
)
|
||||
request_middleware = Middleware.convert(
|
||||
self.request_middleware,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextvars import ContextVar
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
|
@ -23,6 +24,7 @@ from sanic.models.http_types import Credentials
|
|||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.handlers import RequestManager
|
||||
from sanic.server import ConnInfo
|
||||
from sanic.app import Sanic
|
||||
|
||||
|
@ -37,7 +39,7 @@ from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
|||
from httptools import parse_url
|
||||
from httptools.parser.errors import HttpParserInvalidURLError
|
||||
|
||||
from sanic.compat import CancelledErrors, Header
|
||||
from sanic.compat import Header
|
||||
from sanic.constants import (
|
||||
CACHEABLE_HTTP_METHODS,
|
||||
DEFAULT_HTTP_CONTENT_TYPE,
|
||||
|
@ -99,6 +101,7 @@ class Request:
|
|||
"_cookies",
|
||||
"_id",
|
||||
"_ip",
|
||||
"_manager",
|
||||
"_parsed_url",
|
||||
"_port",
|
||||
"_protocol",
|
||||
|
@ -182,6 +185,7 @@ class Request:
|
|||
self.responded: bool = False
|
||||
self.route: Optional[Route] = None
|
||||
self.stream: Optional[Stream] = None
|
||||
self._manager: Optional[RequestManager] = None
|
||||
self._cookies: Optional[Dict[str, str]] = None
|
||||
self._match_info: Dict[str, Any] = {}
|
||||
self._protocol = None
|
||||
|
@ -243,6 +247,10 @@ class Request:
|
|||
)
|
||||
return self._stream_id
|
||||
|
||||
@property
|
||||
def manager(self):
|
||||
return self._manager
|
||||
|
||||
def reset_response(self):
|
||||
try:
|
||||
if (
|
||||
|
@ -333,19 +341,13 @@ class Request:
|
|||
if isawaitable(response):
|
||||
response = await response # type: ignore
|
||||
# Run response middleware
|
||||
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"
|
||||
if (
|
||||
self._manager
|
||||
and not self._manager.response_middleware_run
|
||||
and self._manager.response_middleware
|
||||
):
|
||||
response = await self._manager.run(
|
||||
partial(self._manager.run_response_middleware, response)
|
||||
)
|
||||
self.responded = True
|
||||
return response
|
||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sanic.handlers import RequestManager
|
||||
from sanic.http.constants import HTTP
|
||||
from sanic.http.http3 import Http3
|
||||
from sanic.touchup.meta import TouchUpMeta
|
||||
|
@ -53,7 +54,7 @@ class HttpProtocolMixin:
|
|||
def _setup(self):
|
||||
self.request: Optional[Request] = None
|
||||
self.access_log = self.app.config.ACCESS_LOG
|
||||
self.request_handler = self.app.handle_request
|
||||
self.request_handler = RequestManager
|
||||
self.error_handler = self.app.error_handler
|
||||
self.request_timeout = self.app.config.REQUEST_TIMEOUT
|
||||
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
|
||||
|
|
|
@ -150,8 +150,11 @@ def test_app_route_raise_value_error(app):
|
|||
|
||||
|
||||
def test_app_handle_request_handler_is_none(app, monkeypatch):
|
||||
mock = Mock()
|
||||
mock.handler = None
|
||||
|
||||
def mockreturn(*args, **kwargs):
|
||||
return Mock(), None, {}
|
||||
return mock, None, {}
|
||||
|
||||
# Not sure how to make app.router.get() return None, so use mock here.
|
||||
monkeypatch.setattr(app.router, "get", mockreturn)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from httpx import AsyncByteStream
|
||||
from sanic_testing.reusable import ReusableClient
|
||||
|
||||
from sanic.response import json, text
|
||||
|
|
Loading…
Reference in New Issue
Block a user