Compare commits
7 Commits
middleware
...
v22.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5052321801 | ||
|
|
23ce4eaaa4 | ||
|
|
23a430c4ad | ||
|
|
ec158ffa69 | ||
|
|
6e32270036 | ||
|
|
43ba381e7b | ||
|
|
16503319e5 |
@@ -1 +1 @@
|
|||||||
__version__ = "22.9.1"
|
__version__ = "22.9.0"
|
||||||
|
|||||||
387
sanic/app.py
387
sanic/app.py
@@ -21,6 +21,7 @@ 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,
|
||||||
@@ -46,14 +47,19 @@ from sanic_routing.exceptions import FinalizationError, NotFound
|
|||||||
from sanic_routing.route import Route
|
from sanic_routing.route import Route
|
||||||
|
|
||||||
from sanic.application.ext import setup_ext
|
from sanic.application.ext import setup_ext
|
||||||
from sanic.application.state import ApplicationState, Mode, ServerStage
|
from sanic.application.state import ApplicationState, ServerStage
|
||||||
from sanic.asgi import ASGIApp
|
from sanic.asgi import ASGIApp
|
||||||
from sanic.base.root import BaseSanic
|
from sanic.base.root import BaseSanic
|
||||||
from sanic.blueprint_group import BlueprintGroup
|
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 BadRequest, SanicException, URLBuildError
|
from sanic.exceptions import (
|
||||||
|
BadRequest,
|
||||||
|
SanicException,
|
||||||
|
ServerError,
|
||||||
|
URLBuildError,
|
||||||
|
)
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic.helpers import _default
|
from sanic.helpers import _default
|
||||||
from sanic.http import Stage
|
from sanic.http import Stage
|
||||||
@@ -77,7 +83,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
|
from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream
|
||||||
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
|
||||||
@@ -152,7 +158,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
_app_registry: Dict[str, "Sanic"] = {}
|
_app_registry: Dict[str, "Sanic"] = {}
|
||||||
_uvloop_setting = None # TODO: Remove in v22.6
|
|
||||||
test_mode = False
|
test_mode = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -388,8 +393,8 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
routes = [routes]
|
routes = [routes]
|
||||||
|
|
||||||
for r in routes:
|
for r in routes:
|
||||||
r.ctx.websocket = websocket
|
r.extra.websocket = websocket
|
||||||
r.ctx.static = params.get("static", False)
|
r.extra.static = params.get("static", False)
|
||||||
r.ctx.__dict__.update(ctx)
|
r.ctx.__dict__.update(ctx)
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
@@ -583,7 +588,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
uri = route.path
|
uri = route.path
|
||||||
|
|
||||||
if getattr(route.ctx, "static", None):
|
if getattr(route.extra, "static", None):
|
||||||
filename = kwargs.pop("filename", "")
|
filename = kwargs.pop("filename", "")
|
||||||
# it's static folder
|
# it's static folder
|
||||||
if "__file_uri__" in uri:
|
if "__file_uri__" in uri:
|
||||||
@@ -616,18 +621,18 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
host = kwargs.pop("_host", None)
|
host = kwargs.pop("_host", None)
|
||||||
external = kwargs.pop("_external", False) or bool(host)
|
external = kwargs.pop("_external", False) or bool(host)
|
||||||
scheme = kwargs.pop("_scheme", "")
|
scheme = kwargs.pop("_scheme", "")
|
||||||
if route.ctx.hosts and external:
|
if route.extra.hosts and external:
|
||||||
if not host and len(route.ctx.hosts) > 1:
|
if not host and len(route.extra.hosts) > 1:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Host is ambiguous: {', '.join(route.ctx.hosts)}"
|
f"Host is ambiguous: {', '.join(route.extra.hosts)}"
|
||||||
)
|
)
|
||||||
elif host and host not in route.ctx.hosts:
|
elif host and host not in route.extra.hosts:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Requested host ({host}) is not available for this "
|
f"Requested host ({host}) is not available for this "
|
||||||
f"route: {route.ctx.hosts}"
|
f"route: {route.extra.hosts}"
|
||||||
)
|
)
|
||||||
elif not host:
|
elif not host:
|
||||||
host = list(route.ctx.hosts)[0]
|
host = list(route.extra.hosts)[0]
|
||||||
|
|
||||||
if scheme and not external:
|
if scheme and not external:
|
||||||
raise ValueError("When specifying _scheme, _external must be True")
|
raise ValueError("When specifying _scheme, _external must be True")
|
||||||
@@ -708,10 +713,276 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
exception: BaseException,
|
exception: BaseException,
|
||||||
run_middleware: bool = True,
|
run_middleware: bool = True,
|
||||||
): # no cov
|
): # no cov
|
||||||
raise NotImplementedError
|
"""
|
||||||
|
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
|
||||||
raise NotImplementedError
|
"""Take a request from the HTTP Server and return a response object
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
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.extra.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
|
||||||
@@ -1074,18 +1345,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
def debug(self):
|
def debug(self):
|
||||||
return self.state.is_debug
|
return self.state.is_debug
|
||||||
|
|
||||||
@debug.setter
|
|
||||||
def debug(self, value: bool):
|
|
||||||
deprecation(
|
|
||||||
"Setting the value of a Sanic application's debug value directly "
|
|
||||||
"is deprecated and will be removed in v22.9. Please set it using "
|
|
||||||
"the CLI, app.run, app.prepare, or directly set "
|
|
||||||
"app.state.mode to Mode.DEBUG.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
mode = Mode.DEBUG if value else Mode.PRODUCTION
|
|
||||||
self.state.mode = mode
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_reload(self):
|
def auto_reload(self):
|
||||||
return self.config.AUTO_RELOAD
|
return self.config.AUTO_RELOAD
|
||||||
@@ -1102,58 +1361,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
"""
|
"""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@property
|
|
||||||
def is_running(self):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_running property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
return self.state.is_running
|
|
||||||
|
|
||||||
@is_running.setter
|
|
||||||
def is_running(self, value: bool):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_running property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
self.state.is_running = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_stopping(self):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_stopping property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
return self.state.is_stopping
|
|
||||||
|
|
||||||
@is_stopping.setter
|
|
||||||
def is_stopping(self, value: bool):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_stopping property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
self.state.is_stopping = value
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reload_dirs(self):
|
def reload_dirs(self):
|
||||||
return self.state.reload_dirs
|
return self.state.reload_dirs
|
||||||
@@ -1248,6 +1455,16 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
return cls(name)
|
return cls(name)
|
||||||
raise SanicException(f'Sanic app name "{name}" not found.')
|
raise SanicException(f'Sanic app name "{name}" not found.')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _check_uvloop_conflict(cls) -> None:
|
||||||
|
values = {app.config.USE_UVLOOP for app in cls._app_registry.values()}
|
||||||
|
if len(values) > 1:
|
||||||
|
error_logger.warning(
|
||||||
|
"It looks like you're running several apps with different "
|
||||||
|
"uvloop settings. This is not supported and may lead to "
|
||||||
|
"unintended behaviour."
|
||||||
|
)
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Lifecycle
|
# Lifecycle
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
@@ -1297,17 +1514,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
23.3,
|
23.3,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Replace in v22.6 to check against apps in app registry
|
Sanic._check_uvloop_conflict()
|
||||||
if (
|
|
||||||
self.__class__._uvloop_setting is not None
|
|
||||||
and self.__class__._uvloop_setting != self.config.USE_UVLOOP
|
|
||||||
):
|
|
||||||
error_logger.warning(
|
|
||||||
"It looks like you're running several apps with different "
|
|
||||||
"uvloop settings. This is not supported and may lead to "
|
|
||||||
"unintended behaviour."
|
|
||||||
)
|
|
||||||
self.__class__._uvloop_setting = self.config.USE_UVLOOP
|
|
||||||
|
|
||||||
# Startup time optimizations
|
# Startup time optimizations
|
||||||
if self.state.primary:
|
if self.state.primary:
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from urllib.parse import quote
|
|||||||
|
|
||||||
from sanic.compat import Header
|
from sanic.compat import Header
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.handlers import 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 logger
|
from sanic.log import logger
|
||||||
@@ -231,9 +230,11 @@ class ASGIApp:
|
|||||||
"""
|
"""
|
||||||
Handle the incoming request.
|
Handle the incoming request.
|
||||||
"""
|
"""
|
||||||
manager = RequestManager.create(self.request)
|
|
||||||
try:
|
try:
|
||||||
self.stage = Stage.HANDLER
|
self.stage = Stage.HANDLER
|
||||||
await manager.handle()
|
await self.sanic_app.handle_request(self.request)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await manager.error(e)
|
try:
|
||||||
|
await self.sanic_app.handle_exception(self.request, e)
|
||||||
|
except Exception as exc:
|
||||||
|
await self.sanic_app.handle_exception(self.request, exc, False)
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ class Blueprint(BaseSanic):
|
|||||||
|
|
||||||
self.routes += [route for route in routes if isinstance(route, Route)]
|
self.routes += [route for route in routes if isinstance(route, Route)]
|
||||||
self.websocket_routes += [
|
self.websocket_routes += [
|
||||||
route for route in self.routes if route.ctx.websocket
|
route for route in self.routes if route.extra.websocket
|
||||||
]
|
]
|
||||||
self.middlewares += middleware
|
self.middlewares += middleware
|
||||||
self.exceptions += exception_handlers
|
self.exceptions += exception_handlers
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from sanic.constants import LocalCertCreator
|
|||||||
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
|
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
|
||||||
from sanic.helpers import Default, _default
|
from sanic.helpers import Default, _default
|
||||||
from sanic.http import Http
|
from sanic.http import Http
|
||||||
from sanic.log import deprecation, error_logger
|
from sanic.log import error_logger
|
||||||
from sanic.utils import load_module_from_file_location, str_to_bool
|
from sanic.utils import load_module_from_file_location, str_to_bool
|
||||||
|
|
||||||
|
|
||||||
@@ -71,10 +71,6 @@ DEFAULT_CONFIG = {
|
|||||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
# These values will be removed from the Config object in v22.6 and moved
|
|
||||||
# to the application state
|
|
||||||
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
|
|
||||||
|
|
||||||
|
|
||||||
class DescriptorMeta(type):
|
class DescriptorMeta(type):
|
||||||
def __init__(cls, *_):
|
def __init__(cls, *_):
|
||||||
@@ -132,6 +128,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
):
|
):
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||||
|
self._configure_warnings()
|
||||||
|
|
||||||
self._converters = [str, str_to_bool, float, int]
|
self._converters = [str, str_to_bool, float, int]
|
||||||
|
|
||||||
@@ -149,7 +146,6 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
self.load_environment_vars(SANIC_PREFIX)
|
self.load_environment_vars(SANIC_PREFIX)
|
||||||
|
|
||||||
self._configure_header_size()
|
self._configure_header_size()
|
||||||
self._configure_warnings()
|
|
||||||
self._check_error_format()
|
self._check_error_format()
|
||||||
self._init = True
|
self._init = True
|
||||||
|
|
||||||
@@ -241,7 +237,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
"""
|
"""
|
||||||
Looks for prefixed environment variables and applies them to the
|
Looks for prefixed environment variables and applies them to the
|
||||||
configuration if present. This is called automatically when Sanic
|
configuration if present. This is called automatically when Sanic
|
||||||
starts up to load environment variables into config.
|
starts up to load environment variables into config. Environment
|
||||||
|
variables should start with the defined prefix and should only
|
||||||
|
contain uppercase letters.
|
||||||
|
|
||||||
It will automatically hydrate the following types:
|
It will automatically hydrate the following types:
|
||||||
|
|
||||||
@@ -267,12 +265,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
`See user guide re: config
|
`See user guide re: config
|
||||||
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
||||||
"""
|
"""
|
||||||
lower_case_var_found = False
|
|
||||||
for key, value in environ.items():
|
for key, value in environ.items():
|
||||||
if not key.startswith(prefix):
|
if not key.startswith(prefix) or not key.isupper():
|
||||||
continue
|
continue
|
||||||
if not key.isupper():
|
|
||||||
lower_case_var_found = True
|
|
||||||
|
|
||||||
_, config_key = key.split(prefix, 1)
|
_, config_key = key.split(prefix, 1)
|
||||||
|
|
||||||
@@ -282,12 +277,6 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
break
|
break
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
if lower_case_var_found:
|
|
||||||
deprecation(
|
|
||||||
"Lowercase environment variables will not be "
|
|
||||||
"loaded into Sanic config beginning in v22.9.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -448,8 +448,8 @@ def exception_response(
|
|||||||
# from the route
|
# from the route
|
||||||
if request.route:
|
if request.route:
|
||||||
try:
|
try:
|
||||||
if request.route.ctx.error_format:
|
if request.route.extra.error_format:
|
||||||
render_format = request.route.ctx.error_format
|
render_format = request.route.extra.error_format
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
@@ -1,317 +1,16 @@
|
|||||||
from __future__ import annotations
|
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 typing import Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
from sanic_routing import Route
|
|
||||||
|
|
||||||
from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
|
from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
HeaderNotFound,
|
HeaderNotFound,
|
||||||
InvalidRangeType,
|
InvalidRangeType,
|
||||||
RangeNotSatisfiable,
|
RangeNotSatisfiable,
|
||||||
SanicException,
|
|
||||||
ServerError,
|
|
||||||
)
|
)
|
||||||
from sanic.http.constants import Stage
|
from sanic.log import deprecation, error_logger
|
||||||
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.response import text
|
||||||
from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream, text
|
|
||||||
from sanic.touchup import TouchUpMeta
|
|
||||||
|
|
||||||
|
|
||||||
class RequestHandler:
|
|
||||||
def __init__(self, func, request_middleware, response_middleware):
|
|
||||||
self.func = func.func if isinstance(func, RequestHandler) else func
|
|
||||||
self.request_middleware = request_middleware
|
|
||||||
self.response_middleware = response_middleware
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
return self.func(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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, raise_exception: bool = False):
|
|
||||||
response: Optional[BaseHTTPResponse] = None
|
|
||||||
if not self.request_middleware_run and self.request_middleware:
|
|
||||||
response = await self.run(
|
|
||||||
self.run_request_middleware, raise_exception
|
|
||||||
)
|
|
||||||
|
|
||||||
if not response:
|
|
||||||
# Run response handler
|
|
||||||
response = await self.run(handler, raise_exception)
|
|
||||||
|
|
||||||
if not self.response_middleware_run and self.response_middleware:
|
|
||||||
response = await self.run(
|
|
||||||
partial(self.run_response_middleware, response),
|
|
||||||
raise_exception,
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.cleanup(response)
|
|
||||||
|
|
||||||
async def run(
|
|
||||||
self, operation, raise_exception: bool = False
|
|
||||||
) -> Optional[BaseHTTPResponse]:
|
|
||||||
try:
|
|
||||||
response = operation()
|
|
||||||
if isawaitable(response):
|
|
||||||
response = await response
|
|
||||||
except Exception as e:
|
|
||||||
if raise_exception:
|
|
||||||
raise
|
|
||||||
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), True
|
|
||||||
)
|
|
||||||
except Exception as 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:
|
|
||||||
error_logger.exception(e)
|
|
||||||
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
|
|
||||||
|
|
||||||
if isinstance(response, BaseHTTPResponse):
|
|
||||||
await self.request.app.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 response.eof() # type: ignore
|
|
||||||
await self.request.app.dispatch(
|
|
||||||
"http.lifecycle.response",
|
|
||||||
inline=True,
|
|
||||||
context={"request": self.request, "response": response},
|
|
||||||
)
|
|
||||||
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.request.app.dispatch(
|
|
||||||
"http.middleware.before",
|
|
||||||
inline=True,
|
|
||||||
context={"request": self.request, "response": None},
|
|
||||||
condition={"attach_to": "request"},
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = await self.run(partial(middleware, self.request))
|
|
||||||
except Exception:
|
|
||||||
error_logger.exception(
|
|
||||||
"Exception occurred in one of request middleware handlers"
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|
||||||
await self.request.app.dispatch(
|
|
||||||
"http.middleware.after",
|
|
||||||
inline=True,
|
|
||||||
context={"request": self.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.request.app.dispatch(
|
|
||||||
"http.middleware.before",
|
|
||||||
inline=True,
|
|
||||||
context={"request": self.request, "response": None},
|
|
||||||
condition={"attach_to": "request"},
|
|
||||||
)
|
|
||||||
|
|
||||||
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.request.app.dispatch(
|
|
||||||
"http.middleware.after",
|
|
||||||
inline=True,
|
|
||||||
context={"request": self.request, "response": None},
|
|
||||||
condition={"attach_to": "request"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if resp:
|
|
||||||
return resp
|
|
||||||
return response
|
|
||||||
|
|
||||||
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 handler and handler.request_middleware:
|
|
||||||
self.request_middleware = handler.request_middleware
|
|
||||||
|
|
||||||
if handler and handler.response_middleware:
|
|
||||||
self.response_middleware = handler.response_middleware
|
|
||||||
|
|
||||||
return route
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _noop(_):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorHandler:
|
class ErrorHandler:
|
||||||
|
|||||||
@@ -124,8 +124,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
self.stage = Stage.HANDLER
|
self.stage = Stage.HANDLER
|
||||||
self.request.conn_info = self.protocol.conn_info
|
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
|
# Handler finished, response should've been sent
|
||||||
if self.stage is Stage.HANDLER and not self.upgrade_websocket:
|
if self.stage is Stage.HANDLER and not self.upgrade_websocket:
|
||||||
@@ -251,7 +250,6 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||||||
transport=self.protocol.transport,
|
transport=self.protocol.transport,
|
||||||
app=self.protocol.app,
|
app=self.protocol.app,
|
||||||
)
|
)
|
||||||
self.protocol.request_handler.create(request)
|
|
||||||
self.protocol.request_class._current.set(request)
|
self.protocol.request_class._current.set(request)
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.lifecycle.request",
|
"http.lifecycle.request",
|
||||||
@@ -425,11 +423,15 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
# From request and handler states we can respond, otherwise be silent
|
# From request and handler states we can respond, otherwise be silent
|
||||||
if self.stage is Stage.HANDLER:
|
if self.stage is Stage.HANDLER:
|
||||||
|
app = self.protocol.app
|
||||||
|
|
||||||
if self.request is None:
|
if self.request is None:
|
||||||
self.create_empty_request()
|
self.create_empty_request()
|
||||||
self.protocol.request_handler.create(self.request)
|
|
||||||
|
|
||||||
await self.request.manager.error(exception)
|
try:
|
||||||
|
await app.handle_exception(self.request, exception)
|
||||||
|
except Exception as e:
|
||||||
|
await app.handle_exception(self.request, e, False)
|
||||||
|
|
||||||
def create_empty_request(self) -> None:
|
def create_empty_request(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
12
sanic/log.py
12
sanic/log.py
@@ -25,6 +25,12 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
|||||||
"propagate": True,
|
"propagate": True,
|
||||||
"qualname": "sanic.access",
|
"qualname": "sanic.access",
|
||||||
},
|
},
|
||||||
|
"sanic.server": {
|
||||||
|
"level": "INFO",
|
||||||
|
"handlers": ["console"],
|
||||||
|
"propagate": True,
|
||||||
|
"qualname": "sanic.server",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handlers={
|
handlers={
|
||||||
"console": {
|
"console": {
|
||||||
@@ -101,6 +107,12 @@ Logger used by Sanic for access logging
|
|||||||
"""
|
"""
|
||||||
access_logger.addFilter(_verbosity_filter)
|
access_logger.addFilter(_verbosity_filter)
|
||||||
|
|
||||||
|
server_logger = logging.getLogger("sanic.server") # no cov
|
||||||
|
"""
|
||||||
|
Logger used by Sanic for server related messages
|
||||||
|
"""
|
||||||
|
logger.addFilter(_verbosity_filter)
|
||||||
|
|
||||||
|
|
||||||
def deprecation(message: str, version: float): # no cov
|
def deprecation(message: str, version: float): # no cov
|
||||||
version_info = f"[DEPRECATION v{version}] "
|
version_info = f"[DEPRECATION v{version}] "
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Middleware:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
func: MiddlewareType,
|
func: MiddlewareType,
|
||||||
location: MiddlewareLocation = MiddlewareLocation.REQUEST,
|
location: MiddlewareLocation,
|
||||||
priority: int = 0,
|
priority: int = 0,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.func = func
|
self.func = func
|
||||||
@@ -33,10 +33,9 @@ class Middleware:
|
|||||||
return self.func(*args, **kwargs)
|
return self.func(*args, **kwargs)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
name = getattr(self.func, "__name__", str(self.func))
|
|
||||||
return (
|
return (
|
||||||
f"{self.__class__.__name__}("
|
f"{self.__class__.__name__}("
|
||||||
f"func=<function {name}>, "
|
f"func=<function {self.func.__name__}>, "
|
||||||
f"priority={self.priority}, "
|
f"priority={self.priority}, "
|
||||||
f"location={self.location.name})"
|
f"location={self.location.name})"
|
||||||
)
|
)
|
||||||
@@ -64,3 +63,4 @@ class Middleware:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def reset_count(cls):
|
def reset_count(cls):
|
||||||
cls._counter = count()
|
cls._counter = count()
|
||||||
|
cls.count = next(cls._counter)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from operator import attrgetter
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
from sanic.base.meta import SanicMeta
|
||||||
from sanic.handlers import RequestHandler
|
|
||||||
from sanic.middleware import Middleware, MiddlewareLocation
|
from sanic.middleware import Middleware, MiddlewareLocation
|
||||||
from sanic.models.futures import FutureMiddleware
|
from sanic.models.futures import FutureMiddleware
|
||||||
from sanic.router import Router
|
from sanic.router import Router
|
||||||
@@ -105,23 +104,19 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
|||||||
self.named_response_middleware.get(route.name, deque()),
|
self.named_response_middleware.get(route.name, deque()),
|
||||||
location=MiddlewareLocation.RESPONSE,
|
location=MiddlewareLocation.RESPONSE,
|
||||||
)
|
)
|
||||||
|
route.extra.request_middleware = deque(
|
||||||
route.handler = RequestHandler(
|
sorted(
|
||||||
route.handler,
|
request_middleware,
|
||||||
deque(
|
key=attrgetter("order"),
|
||||||
sorted(
|
reverse=True,
|
||||||
request_middleware,
|
)
|
||||||
key=attrgetter("order"),
|
)
|
||||||
reverse=True,
|
route.extra.response_middleware = deque(
|
||||||
)
|
sorted(
|
||||||
),
|
response_middleware,
|
||||||
deque(
|
key=attrgetter("order"),
|
||||||
sorted(
|
reverse=True,
|
||||||
response_middleware,
|
)[::-1]
|
||||||
key=attrgetter("order"),
|
|
||||||
reverse=True,
|
|
||||||
)[::-1]
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
request_middleware = Middleware.convert(
|
request_middleware = Middleware.convert(
|
||||||
self.request_middleware,
|
self.request_middleware,
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
from ast import NodeVisitor, Return, parse
|
from ast import NodeVisitor, Return, parse
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from email.utils import formatdate
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
from inspect import getsource, signature
|
from inspect import getsource, signature
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from time import gmtime, strftime
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
|
Dict,
|
||||||
Iterable,
|
Iterable,
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
@@ -31,21 +32,13 @@ from sanic.handlers import ContentRangeHandler
|
|||||||
from sanic.log import error_logger
|
from sanic.log import error_logger
|
||||||
from sanic.models.futures import FutureRoute, FutureStatic
|
from sanic.models.futures import FutureRoute, FutureStatic
|
||||||
from sanic.models.handler_types import RouteHandler
|
from sanic.models.handler_types import RouteHandler
|
||||||
from sanic.response import HTTPResponse, file, file_stream
|
from sanic.response import HTTPResponse, file, file_stream, validate_file
|
||||||
from sanic.types import HashableDict
|
from sanic.types import HashableDict
|
||||||
|
|
||||||
|
|
||||||
RouteWrapper = Callable[
|
RouteWrapper = Callable[
|
||||||
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
||||||
]
|
]
|
||||||
RESTRICTED_ROUTE_CONTEXT = (
|
|
||||||
"ignore_body",
|
|
||||||
"stream",
|
|
||||||
"hosts",
|
|
||||||
"static",
|
|
||||||
"error_format",
|
|
||||||
"websocket",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RouteMixin(metaclass=SanicMeta):
|
class RouteMixin(metaclass=SanicMeta):
|
||||||
@@ -790,24 +783,9 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
return name
|
return name
|
||||||
|
|
||||||
async def _static_request_handler(
|
async def _get_file_path(self, file_or_directory, __file_uri__, not_found):
|
||||||
self,
|
|
||||||
file_or_directory,
|
|
||||||
use_modified_since,
|
|
||||||
use_content_range,
|
|
||||||
stream_large_files,
|
|
||||||
request,
|
|
||||||
content_type=None,
|
|
||||||
__file_uri__=None,
|
|
||||||
):
|
|
||||||
# Merge served directory and requested file if provided
|
|
||||||
file_path_raw = Path(unquote(file_or_directory))
|
file_path_raw = Path(unquote(file_or_directory))
|
||||||
root_path = file_path = file_path_raw.resolve()
|
root_path = file_path = file_path_raw.resolve()
|
||||||
not_found = FileNotFound(
|
|
||||||
"File not found",
|
|
||||||
path=file_or_directory,
|
|
||||||
relative_url=__file_uri__,
|
|
||||||
)
|
|
||||||
|
|
||||||
if __file_uri__:
|
if __file_uri__:
|
||||||
# Strip all / that in the beginning of the URL to help prevent
|
# Strip all / that in the beginning of the URL to help prevent
|
||||||
@@ -834,6 +812,29 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
f"relative_url={__file_uri__}"
|
f"relative_url={__file_uri__}"
|
||||||
)
|
)
|
||||||
raise not_found
|
raise not_found
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
async def _static_request_handler(
|
||||||
|
self,
|
||||||
|
file_or_directory,
|
||||||
|
use_modified_since,
|
||||||
|
use_content_range,
|
||||||
|
stream_large_files,
|
||||||
|
request,
|
||||||
|
content_type=None,
|
||||||
|
__file_uri__=None,
|
||||||
|
):
|
||||||
|
not_found = FileNotFound(
|
||||||
|
"File not found",
|
||||||
|
path=file_or_directory,
|
||||||
|
relative_url=__file_uri__,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Merge served directory and requested file if provided
|
||||||
|
file_path = await self._get_file_path(
|
||||||
|
file_or_directory, __file_uri__, not_found
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers = {}
|
headers = {}
|
||||||
# Check if the client has been sent this file before
|
# Check if the client has been sent this file before
|
||||||
@@ -841,15 +842,13 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
stats = None
|
stats = None
|
||||||
if use_modified_since:
|
if use_modified_since:
|
||||||
stats = await stat_async(file_path)
|
stats = await stat_async(file_path)
|
||||||
modified_since = strftime(
|
modified_since = stats.st_mtime
|
||||||
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
|
response = await validate_file(request.headers, modified_since)
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
headers["Last-Modified"] = formatdate(
|
||||||
|
modified_since, usegmt=True
|
||||||
)
|
)
|
||||||
if (
|
|
||||||
request.headers.getone("if-modified-since", None)
|
|
||||||
== modified_since
|
|
||||||
):
|
|
||||||
return HTTPResponse(status=304)
|
|
||||||
headers["Last-Modified"] = modified_since
|
|
||||||
_range = None
|
_range = None
|
||||||
if use_content_range:
|
if use_content_range:
|
||||||
_range = None
|
_range = None
|
||||||
@@ -864,8 +863,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
del headers["Content-Length"]
|
del headers["Content-Length"]
|
||||||
for key, value in _range.headers.items():
|
headers.update(_range.headers)
|
||||||
headers[key] = value
|
|
||||||
|
|
||||||
if "content-type" not in headers:
|
if "content-type" not in headers:
|
||||||
content_type = (
|
content_type = (
|
||||||
@@ -1041,24 +1039,12 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
return types
|
return types
|
||||||
|
|
||||||
def _build_route_context(self, raw):
|
def _build_route_context(self, raw: Dict[str, Any]) -> HashableDict:
|
||||||
ctx_kwargs = {
|
ctx_kwargs = {
|
||||||
key.replace("ctx_", ""): raw.pop(key)
|
key.replace("ctx_", ""): raw.pop(key)
|
||||||
for key in {**raw}.keys()
|
for key in {**raw}.keys()
|
||||||
if key.startswith("ctx_")
|
if key.startswith("ctx_")
|
||||||
}
|
}
|
||||||
restricted = [
|
|
||||||
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
|
|
||||||
]
|
|
||||||
if restricted:
|
|
||||||
restricted_arguments = ", ".join(restricted)
|
|
||||||
raise AttributeError(
|
|
||||||
"Cannot use restricted route context: "
|
|
||||||
f"{restricted_arguments}. This limitation is only in place "
|
|
||||||
"until v22.9 when the restricted names will no longer be in"
|
|
||||||
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
|
|
||||||
"for more information."
|
|
||||||
)
|
|
||||||
if raw:
|
if raw:
|
||||||
unexpected_arguments = ", ".join(raw.keys())
|
unexpected_arguments = ", ".join(raw.keys())
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from typing import (
|
|||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from sanic.application.ext import setup_ext
|
||||||
from sanic.application.logo import get_logo
|
from sanic.application.logo import get_logo
|
||||||
from sanic.application.motd import MOTD
|
from sanic.application.motd import MOTD
|
||||||
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
|
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
|
||||||
@@ -558,7 +559,6 @@ class StartupMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
def motd(
|
def motd(
|
||||||
self,
|
self,
|
||||||
serve_location: str = "",
|
|
||||||
server_settings: Optional[Dict[str, Any]] = None,
|
server_settings: Optional[Dict[str, Any]] = None,
|
||||||
):
|
):
|
||||||
if (
|
if (
|
||||||
@@ -568,14 +568,7 @@ class StartupMixin(metaclass=SanicMeta):
|
|||||||
or os.environ.get("SANIC_SERVER_RUNNING")
|
or os.environ.get("SANIC_SERVER_RUNNING")
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
if serve_location:
|
serve_location = self.get_server_location(server_settings)
|
||||||
deprecation(
|
|
||||||
"Specifying a serve_location in the MOTD is deprecated and "
|
|
||||||
"will be removed.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
serve_location = self.get_server_location(server_settings)
|
|
||||||
if self.config.MOTD:
|
if self.config.MOTD:
|
||||||
logo = get_logo(coffee=self.state.coffee)
|
logo = get_logo(coffee=self.state.coffee)
|
||||||
display, extra = self.get_motd_data(server_settings)
|
display, extra = self.get_motd_data(server_settings)
|
||||||
@@ -746,9 +739,12 @@ class StartupMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
socks = []
|
socks = []
|
||||||
sync_manager = Manager()
|
sync_manager = Manager()
|
||||||
|
setup_ext(primary)
|
||||||
try:
|
try:
|
||||||
main_start = primary_server_info.settings.pop("main_start", None)
|
primary_server_info.settings.pop("main_start", None)
|
||||||
main_stop = primary_server_info.settings.pop("main_stop", None)
|
primary_server_info.settings.pop("main_stop", None)
|
||||||
|
main_start = primary.listeners.get("main_process_start")
|
||||||
|
main_stop = primary.listeners.get("main_process_stop")
|
||||||
app = primary_server_info.settings.pop("app")
|
app = primary_server_info.settings.pop("app")
|
||||||
app.setup_loop()
|
app.setup_loop()
|
||||||
loop = new_event_loop()
|
loop = new_event_loop()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from functools import partial
|
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
@@ -24,7 +23,6 @@ from sanic.models.http_types import Credentials
|
|||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sanic.handlers import RequestManager
|
|
||||||
from sanic.server import ConnInfo
|
from sanic.server import ConnInfo
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
|
|
||||||
@@ -39,7 +37,7 @@ from urllib.parse import parse_qs, parse_qsl, unquote, urlunparse
|
|||||||
from httptools import parse_url
|
from httptools import parse_url
|
||||||
from httptools.parser.errors import HttpParserInvalidURLError
|
from httptools.parser.errors import HttpParserInvalidURLError
|
||||||
|
|
||||||
from sanic.compat import Header
|
from sanic.compat import CancelledErrors, Header
|
||||||
from sanic.constants import (
|
from sanic.constants import (
|
||||||
CACHEABLE_HTTP_METHODS,
|
CACHEABLE_HTTP_METHODS,
|
||||||
DEFAULT_HTTP_CONTENT_TYPE,
|
DEFAULT_HTTP_CONTENT_TYPE,
|
||||||
@@ -101,7 +99,6 @@ class Request:
|
|||||||
"_cookies",
|
"_cookies",
|
||||||
"_id",
|
"_id",
|
||||||
"_ip",
|
"_ip",
|
||||||
"_manager",
|
|
||||||
"_parsed_url",
|
"_parsed_url",
|
||||||
"_port",
|
"_port",
|
||||||
"_protocol",
|
"_protocol",
|
||||||
@@ -185,7 +182,6 @@ class Request:
|
|||||||
self.responded: bool = False
|
self.responded: bool = False
|
||||||
self.route: Optional[Route] = None
|
self.route: Optional[Route] = None
|
||||||
self.stream: Optional[Stream] = None
|
self.stream: Optional[Stream] = None
|
||||||
self._manager: Optional[RequestManager] = None
|
|
||||||
self._cookies: Optional[Dict[str, str]] = None
|
self._cookies: Optional[Dict[str, str]] = None
|
||||||
self._match_info: Dict[str, Any] = {}
|
self._match_info: Dict[str, Any] = {}
|
||||||
self._protocol = None
|
self._protocol = None
|
||||||
@@ -229,7 +225,7 @@ class Request:
|
|||||||
"Request.request_middleware_started has been deprecated and will"
|
"Request.request_middleware_started has been deprecated and will"
|
||||||
"be removed. You should set a flag on the request context using"
|
"be removed. You should set a flag on the request context using"
|
||||||
"either middleware or signals if you need this feature.",
|
"either middleware or signals if you need this feature.",
|
||||||
22.3,
|
23.3,
|
||||||
)
|
)
|
||||||
return self._request_middleware_started
|
return self._request_middleware_started
|
||||||
|
|
||||||
@@ -247,10 +243,6 @@ class Request:
|
|||||||
)
|
)
|
||||||
return self._stream_id
|
return self._stream_id
|
||||||
|
|
||||||
@property
|
|
||||||
def manager(self):
|
|
||||||
return self._manager
|
|
||||||
|
|
||||||
def reset_response(self):
|
def reset_response(self):
|
||||||
try:
|
try:
|
||||||
if (
|
if (
|
||||||
@@ -341,13 +333,19 @@ class Request:
|
|||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response # type: ignore
|
response = await response # type: ignore
|
||||||
# Run response middleware
|
# Run response middleware
|
||||||
if (
|
try:
|
||||||
self._manager
|
middleware = (
|
||||||
and not self._manager.response_middleware_run
|
self.route and self.route.extra.response_middleware
|
||||||
and self._manager.response_middleware
|
) or self.app.response_middleware
|
||||||
):
|
if middleware:
|
||||||
response = await self._manager.run(
|
response = await self.app._run_response_middleware(
|
||||||
partial(self._manager.run_response_middleware, response)
|
self, response, middleware
|
||||||
|
)
|
||||||
|
except CancelledErrors:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
error_logger.exception(
|
||||||
|
"Exception occurred in one of response middleware handlers"
|
||||||
)
|
)
|
||||||
self.responded = True
|
self.responded = True
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -32,11 +31,9 @@ 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, RequestHandler, Dict[str, Any]]:
|
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
# We know this will always be RequestHandler, so we can ignore
|
return self.resolve(
|
||||||
# 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,
|
||||||
@@ -53,7 +50,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, RequestHandler, Dict[str, Any]]:
|
) -> Tuple[Route, RouteHandler, 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
|
||||||
@@ -62,7 +59,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, RequestHandler, Dict[str, Any]]
|
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
|
||||||
"""
|
"""
|
||||||
return self._get(path, method, host)
|
return self._get(path, method, host)
|
||||||
|
|
||||||
@@ -117,7 +114,7 @@ class Router(BaseRouter):
|
|||||||
|
|
||||||
params = dict(
|
params = dict(
|
||||||
path=uri,
|
path=uri,
|
||||||
handler=RequestHandler(handler, [], []),
|
handler=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,
|
||||||
@@ -136,14 +133,14 @@ class Router(BaseRouter):
|
|||||||
params.update({"requirements": {"host": host}})
|
params.update({"requirements": {"host": host}})
|
||||||
|
|
||||||
route = super().add(**params) # type: ignore
|
route = super().add(**params) # type: ignore
|
||||||
route.ctx.ignore_body = ignore_body
|
route.extra.ignore_body = ignore_body
|
||||||
route.ctx.stream = stream
|
route.extra.stream = stream
|
||||||
route.ctx.hosts = hosts
|
route.extra.hosts = hosts
|
||||||
route.ctx.static = static
|
route.extra.static = static
|
||||||
route.ctx.error_format = error_format
|
route.extra.error_format = error_format
|
||||||
|
|
||||||
if error_format:
|
if error_format:
|
||||||
check_error_format(route.ctx.error_format)
|
check_error_format(route.extra.error_format)
|
||||||
|
|
||||||
routes.append(route)
|
routes.append(route)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from sanic.handlers import RequestManager
|
|
||||||
from sanic.http.constants import HTTP
|
from sanic.http.constants import HTTP
|
||||||
from sanic.http.http3 import Http3
|
from sanic.http.http3 import Http3
|
||||||
from sanic.touchup.meta import TouchUpMeta
|
from sanic.touchup.meta import TouchUpMeta
|
||||||
@@ -58,7 +57,7 @@ class HttpProtocolMixin:
|
|||||||
def _setup(self):
|
def _setup(self):
|
||||||
self.request: Optional[Request] = None
|
self.request: Optional[Request] = None
|
||||||
self.access_log = self.app.config.ACCESS_LOG
|
self.access_log = self.app.config.ACCESS_LOG
|
||||||
self.request_handler = RequestManager
|
self.request_handler = self.app.handle_request
|
||||||
self.error_handler = self.app.error_handler
|
self.error_handler = self.app.error_handler
|
||||||
self.request_timeout = self.app.config.REQUEST_TIMEOUT
|
self.request_timeout = self.app.config.REQUEST_TIMEOUT
|
||||||
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
|
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from signal import signal as signal_func
|
|||||||
from sanic.application.ext import setup_ext
|
from sanic.application.ext import setup_ext
|
||||||
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
||||||
from sanic.http.http3 import SessionTicketStore, get_config
|
from sanic.http.http3 import SessionTicketStore, get_config
|
||||||
from sanic.log import error_logger, logger
|
from sanic.log import error_logger, server_logger
|
||||||
from sanic.models.server_types import Signal
|
from sanic.models.server_types import Signal
|
||||||
from sanic.server.async_server import AsyncioServer
|
from sanic.server.async_server import AsyncioServer
|
||||||
from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol
|
from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol
|
||||||
@@ -149,12 +149,12 @@ def _setup_system_signals(
|
|||||||
def _run_server_forever(loop, before_stop, after_stop, cleanup, unix):
|
def _run_server_forever(loop, before_stop, after_stop, cleanup, unix):
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
try:
|
try:
|
||||||
logger.info("Starting worker [%s]", pid)
|
server_logger.info("Starting worker [%s]", pid)
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
logger.info("Stopping worker [%s]", pid)
|
server_logger.info("Stopping worker [%s]", pid)
|
||||||
|
|
||||||
loop.run_until_complete(before_stop())
|
loop.run_until_complete(before_stop())
|
||||||
|
|
||||||
@@ -372,7 +372,9 @@ def serve_multiple(server_settings, workers):
|
|||||||
processes = []
|
processes = []
|
||||||
|
|
||||||
def sig_handler(signal, frame):
|
def sig_handler(signal, frame):
|
||||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
server_logger.info(
|
||||||
|
"Received signal %s. Shutting down.", Signals(signal).name
|
||||||
|
)
|
||||||
for process in processes:
|
for process in processes:
|
||||||
os.kill(process.pid, SIGTERM)
|
os.kill(process.pid, SIGTERM)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from websockets.connection import CLOSED, CLOSING, OPEN, Event
|
from websockets.connection import CLOSED, CLOSING, OPEN, Event
|
||||||
from websockets.exceptions import ConnectionClosed, ConnectionClosedError
|
from websockets.exceptions import (
|
||||||
|
ConnectionClosed,
|
||||||
|
ConnectionClosedError,
|
||||||
|
ConnectionClosedOK,
|
||||||
|
)
|
||||||
from websockets.frames import Frame, Opcode
|
from websockets.frames import Frame, Opcode
|
||||||
from websockets.server import ServerConnection
|
from websockets.server import ServerConnection
|
||||||
from websockets.typing import Data
|
from websockets.typing import Data
|
||||||
@@ -840,3 +844,10 @@ class WebsocketImplProtocol:
|
|||||||
self.abort_pings()
|
self.abort_pings()
|
||||||
if self.connection_lost_waiter:
|
if self.connection_lost_waiter:
|
||||||
self.connection_lost_waiter.set_result(None)
|
self.connection_lost_waiter.set_result(None)
|
||||||
|
|
||||||
|
async def __aiter__(self):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
yield await self.recv()
|
||||||
|
except ConnectionClosedOK:
|
||||||
|
return
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class Inspector:
|
|||||||
|
|
||||||
def state_to_json(self):
|
def state_to_json(self):
|
||||||
output = {"info": self.app_info}
|
output = {"info": self.app_info}
|
||||||
output["workers"] = self._make_safe(dict(self.worker_state))
|
output["workers"] = self.make_safe(dict(self.worker_state))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
@@ -84,10 +84,11 @@ class Inspector:
|
|||||||
message = "__TERMINATE__"
|
message = "__TERMINATE__"
|
||||||
self._publisher.send(message)
|
self._publisher.send(message)
|
||||||
|
|
||||||
def _make_safe(self, obj: Dict[str, Any]) -> Dict[str, Any]:
|
@staticmethod
|
||||||
|
def make_safe(obj: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
for key, value in obj.items():
|
for key, value in obj.items():
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
obj[key] = self._make_safe(value)
|
obj[key] = Inspector.make_safe(value)
|
||||||
elif isinstance(value, datetime):
|
elif isinstance(value, datetime):
|
||||||
obj[key] = value.isoformat()
|
obj[key] = value.isoformat()
|
||||||
return obj
|
return obj
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -94,7 +94,7 @@ requirements = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
"sanic-testing>=22.9.0b1",
|
"sanic-testing>=22.9.0b2",
|
||||||
"pytest",
|
"pytest",
|
||||||
"coverage",
|
"coverage",
|
||||||
"beautifulsoup4",
|
"beautifulsoup4",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from sanic.exceptions import SanicException
|
|||||||
from sanic.helpers import _default
|
from sanic.helpers import _default
|
||||||
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
from sanic.router import Route
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@@ -152,11 +153,13 @@ def test_app_route_raise_value_error(app: Sanic):
|
|||||||
|
|
||||||
|
|
||||||
def test_app_handle_request_handler_is_none(app: Sanic, monkeypatch):
|
def test_app_handle_request_handler_is_none(app: Sanic, monkeypatch):
|
||||||
mock = Mock()
|
app.config.TOUCHUP = False
|
||||||
mock.handler = None
|
route = Mock(spec=Route)
|
||||||
|
route.extra.request_middleware = []
|
||||||
|
route.extra.response_middleware = []
|
||||||
|
|
||||||
def mockreturn(*args, **kwargs):
|
def mockreturn(*args, **kwargs):
|
||||||
return mock, None, {}
|
return route, None, {}
|
||||||
|
|
||||||
monkeypatch.setattr(app.router, "get", mockreturn)
|
monkeypatch.setattr(app.router, "get", mockreturn)
|
||||||
|
|
||||||
@@ -522,7 +525,7 @@ def test_multiple_uvloop_configs_display_warning(caplog):
|
|||||||
|
|
||||||
counter = Counter([(r[1], r[2]) for r in caplog.record_tuples])
|
counter = Counter([(r[1], r[2]) for r in caplog.record_tuples])
|
||||||
|
|
||||||
assert counter[(logging.WARNING, message)] == 2
|
assert counter[(logging.WARNING, message)] == 3
|
||||||
|
|
||||||
|
|
||||||
def test_cannot_run_fast_and_workers(app: Sanic):
|
def test_cannot_run_fast_and_workers(app: Sanic):
|
||||||
|
|||||||
@@ -125,14 +125,9 @@ def test_env_w_custom_converter():
|
|||||||
|
|
||||||
|
|
||||||
def test_env_lowercase():
|
def test_env_lowercase():
|
||||||
with pytest.warns(None) as record:
|
environ["SANIC_test_answer"] = "42"
|
||||||
environ["SANIC_test_answer"] = "42"
|
app = Sanic(name="Test")
|
||||||
app = Sanic(name="Test")
|
assert "test_answer" not in app.config
|
||||||
assert app.config.test_answer == 42
|
|
||||||
assert str(record[0].message) == (
|
|
||||||
"[DEPRECATION v22.9] Lowercase environment variables will not be "
|
|
||||||
"loaded into Sanic config beginning in v22.9."
|
|
||||||
)
|
|
||||||
del environ["SANIC_test_answer"]
|
del environ["SANIC_test_answer"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -97,15 +97,15 @@ def test_auto_fallback_with_content_type(app):
|
|||||||
def test_route_error_format_set_on_auto(app):
|
def test_route_error_format_set_on_auto(app):
|
||||||
@app.get("/text")
|
@app.get("/text")
|
||||||
def text_response(request):
|
def text_response(request):
|
||||||
return text(request.route.ctx.error_format)
|
return text(request.route.extra.error_format)
|
||||||
|
|
||||||
@app.get("/json")
|
@app.get("/json")
|
||||||
def json_response(request):
|
def json_response(request):
|
||||||
return json({"format": request.route.ctx.error_format})
|
return json({"format": request.route.extra.error_format})
|
||||||
|
|
||||||
@app.get("/html")
|
@app.get("/html")
|
||||||
def html_response(request):
|
def html_response(request):
|
||||||
return html(request.route.ctx.error_format)
|
return html(request.route.extra.error_format)
|
||||||
|
|
||||||
_, response = app.test_client.get("/text")
|
_, response = app.test_client.get("/text")
|
||||||
assert response.text == "text"
|
assert response.text == "text"
|
||||||
|
|||||||
@@ -2,22 +2,12 @@ import logging
|
|||||||
|
|
||||||
from asyncio import CancelledError
|
from asyncio import CancelledError
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from unittest.mock import Mock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from sanic.exceptions import NotFound
|
from sanic.exceptions import NotFound
|
||||||
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
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
@@ -193,7 +183,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 == 500
|
assert response.status == 404
|
||||||
# 404 errors are not logged
|
# 404 errors are not logged
|
||||||
assert (
|
assert (
|
||||||
"sanic.error",
|
"sanic.error",
|
||||||
@@ -328,15 +318,6 @@ def test_middleware_return_response(app):
|
|||||||
resp1 = await request.respond()
|
resp1 = await request.respond()
|
||||||
return resp1
|
return resp1
|
||||||
|
|
||||||
app.test_client.get("/")
|
_, response = app.test_client.get("/")
|
||||||
assert response_middleware_run_count == 1
|
assert response_middleware_run_count == 1
|
||||||
assert request_middleware_run_count == 1
|
assert request_middleware_run_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_middleware_object():
|
|
||||||
mock = Mock()
|
|
||||||
middleware = Middleware(mock)
|
|
||||||
middleware(1, 2, 3, answer=42)
|
|
||||||
|
|
||||||
mock.assert_called_once_with(1, 2, 3, answer=42)
|
|
||||||
assert middleware.order == (0, 0)
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from functools import partial
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
from sanic.middleware import Middleware
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +34,12 @@ PRIORITY_TEST_CASES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def reset_middleware():
|
||||||
|
yield
|
||||||
|
Middleware.reset_count()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"expected,priorities",
|
"expected,priorities",
|
||||||
PRIORITY_TEST_CASES,
|
PRIORITY_TEST_CASES,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import os
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate, parsedate_to_datetime
|
||||||
from logging import ERROR, LogRecord
|
from logging import ERROR, LogRecord
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -665,13 +665,11 @@ def test_multiple_responses(
|
|||||||
|
|
||||||
with caplog.at_level(ERROR):
|
with caplog.at_level(ERROR):
|
||||||
_, response = app.test_client.get("/4")
|
_, response = app.test_client.get("/4")
|
||||||
print(response.json)
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert "foo" not in response.text
|
assert "foo" not in response.text
|
||||||
assert "one" in response.headers
|
assert "one" in response.headers
|
||||||
assert response.headers["one"] == "one"
|
assert response.headers["one"] == "one"
|
||||||
|
|
||||||
print(response.headers)
|
|
||||||
assert message_in_records(caplog.records, error_msg2)
|
assert message_in_records(caplog.records, error_msg2)
|
||||||
|
|
||||||
with caplog.at_level(ERROR):
|
with caplog.at_level(ERROR):
|
||||||
@@ -841,10 +839,10 @@ def test_file_validate(app: Sanic, static_file_directory: str):
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
with open(file_path, "a") as f:
|
with open(file_path, "a") as f:
|
||||||
f.write("bar\n")
|
f.write("bar\n")
|
||||||
|
|
||||||
_, response = app.test_client.get(
|
_, response = app.test_client.get(
|
||||||
"/validate", headers={"If-Modified-Since": last_modified}
|
"/validate", headers={"If-Modified-Since": last_modified}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.body == b"foo\nbar\n"
|
assert response.body == b"foo\nbar\n"
|
||||||
|
|
||||||
@@ -921,3 +919,28 @@ def test_file_validating_304_response(
|
|||||||
)
|
)
|
||||||
assert response.status == 304
|
assert response.status == 304
|
||||||
assert response.body == b""
|
assert response.body == b""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"file_name", ["test.file", "decode me.txt", "python.png"]
|
||||||
|
)
|
||||||
|
def test_file_validating_304_response(
|
||||||
|
app: Sanic, file_name: str, static_file_directory: str
|
||||||
|
):
|
||||||
|
app.static("static", Path(static_file_directory) / file_name)
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/static")
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.body == get_file_content(static_file_directory, file_name)
|
||||||
|
last_modified = parsedate_to_datetime(response.headers["Last-Modified"])
|
||||||
|
last_modified += timedelta(seconds=1)
|
||||||
|
_, response = app.test_client.get(
|
||||||
|
"/static",
|
||||||
|
headers={
|
||||||
|
"if-modified-since": formatdate(
|
||||||
|
last_modified.timestamp(), usegmt=True
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status == 304
|
||||||
|
assert response.body == b""
|
||||||
|
|||||||
@@ -503,9 +503,10 @@ def test_stack_trace_on_not_found(app, static_file_directory, caplog):
|
|||||||
counter = Counter([(r[0], r[1]) for r in caplog.record_tuples])
|
counter = Counter([(r[0], r[1]) for r in caplog.record_tuples])
|
||||||
|
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
assert counter[("sanic.root", logging.INFO)] == 11
|
assert counter[("sanic.root", logging.INFO)] == 9
|
||||||
assert counter[("sanic.root", logging.ERROR)] == 0
|
assert counter[("sanic.root", logging.ERROR)] == 0
|
||||||
assert counter[("sanic.error", logging.ERROR)] == 0
|
assert counter[("sanic.error", logging.ERROR)] == 0
|
||||||
|
assert counter[("sanic.server", logging.INFO)] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
|
def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
|
||||||
@@ -521,9 +522,10 @@ def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
|
|||||||
counter = Counter([(r[0], r[1]) for r in caplog.record_tuples])
|
counter = Counter([(r[0], r[1]) for r in caplog.record_tuples])
|
||||||
|
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
assert counter[("sanic.root", logging.INFO)] == 11
|
assert counter[("sanic.root", logging.INFO)] == 9
|
||||||
assert counter[("sanic.root", logging.ERROR)] == 0
|
assert counter[("sanic.root", logging.ERROR)] == 0
|
||||||
assert counter[("sanic.error", logging.ERROR)] == 0
|
assert counter[("sanic.error", logging.ERROR)] == 0
|
||||||
|
assert counter[("sanic.server", logging.INFO)] == 2
|
||||||
assert response.text == "No file: /static/non_existing_file.file"
|
assert response.text == "No file: /static/non_existing_file.file"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
56
tests/test_ws_handlers.py
Normal file
56
tests/test_ws_handlers.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from typing import Any, Callable, Coroutine
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from websockets.client import WebSocketClientProtocol
|
||||||
|
|
||||||
|
from sanic import Request, Sanic, Websocket
|
||||||
|
|
||||||
|
|
||||||
|
MimicClientType = Callable[
|
||||||
|
[WebSocketClientProtocol], Coroutine[None, None, Any]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def simple_ws_mimic_client():
|
||||||
|
async def client_mimic(ws: WebSocketClientProtocol):
|
||||||
|
await ws.send("test 1")
|
||||||
|
await ws.recv()
|
||||||
|
await ws.send("test 2")
|
||||||
|
await ws.recv()
|
||||||
|
|
||||||
|
return client_mimic
|
||||||
|
|
||||||
|
|
||||||
|
def test_ws_handler(
|
||||||
|
app: Sanic,
|
||||||
|
simple_ws_mimic_client: MimicClientType,
|
||||||
|
):
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def ws_echo_handler(request: Request, ws: Websocket):
|
||||||
|
while True:
|
||||||
|
msg = await ws.recv()
|
||||||
|
await ws.send(msg)
|
||||||
|
|
||||||
|
_, ws_proxy = app.test_client.websocket(
|
||||||
|
"/ws", mimic=simple_ws_mimic_client
|
||||||
|
)
|
||||||
|
assert ws_proxy.client_sent == ["test 1", "test 2", ""]
|
||||||
|
assert ws_proxy.client_received == ["test 1", "test 2"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ws_handler_async_for(
|
||||||
|
app: Sanic,
|
||||||
|
simple_ws_mimic_client: MimicClientType,
|
||||||
|
):
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def ws_echo_handler(request: Request, ws: Websocket):
|
||||||
|
async for msg in ws:
|
||||||
|
await ws.send(msg)
|
||||||
|
|
||||||
|
_, ws_proxy = app.test_client.websocket(
|
||||||
|
"/ws", mimic=simple_ws_mimic_client
|
||||||
|
)
|
||||||
|
assert ws_proxy.client_sent == ["test 1", "test 2", ""]
|
||||||
|
assert ws_proxy.client_received == ["test 1", "test 2"]
|
||||||
Reference in New Issue
Block a user