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 os import environ
|
||||
from socket import socket
|
||||
from traceback import format_exc
|
||||
from types import SimpleNamespace
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -46,14 +47,19 @@ from sanic_routing.exceptions import FinalizationError, NotFound
|
||||
from sanic_routing.route import Route
|
||||
|
||||
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.base.root import BaseSanic
|
||||
from sanic.blueprint_group import BlueprintGroup
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
|
||||
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.helpers import _default
|
||||
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 Sanic as SanicVar
|
||||
from sanic.request import Request
|
||||
from sanic.response import BaseHTTPResponse
|
||||
from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream
|
||||
from sanic.router import Router
|
||||
from sanic.server.websockets.impl import ConnectionClosed
|
||||
from sanic.signals import Signal, SignalRouter
|
||||
@@ -152,7 +158,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||
)
|
||||
|
||||
_app_registry: Dict[str, "Sanic"] = {}
|
||||
_uvloop_setting = None # TODO: Remove in v22.6
|
||||
test_mode = False
|
||||
|
||||
def __init__(
|
||||
@@ -388,8 +393,8 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||
routes = [routes]
|
||||
|
||||
for r in routes:
|
||||
r.ctx.websocket = websocket
|
||||
r.ctx.static = params.get("static", False)
|
||||
r.extra.websocket = websocket
|
||||
r.extra.static = params.get("static", False)
|
||||
r.ctx.__dict__.update(ctx)
|
||||
|
||||
return routes
|
||||
@@ -583,7 +588,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||
|
||||
uri = route.path
|
||||
|
||||
if getattr(route.ctx, "static", None):
|
||||
if getattr(route.extra, "static", None):
|
||||
filename = kwargs.pop("filename", "")
|
||||
# it's static folder
|
||||
if "__file_uri__" in uri:
|
||||
@@ -616,18 +621,18 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||
host = kwargs.pop("_host", None)
|
||||
external = kwargs.pop("_external", False) or bool(host)
|
||||
scheme = kwargs.pop("_scheme", "")
|
||||
if route.ctx.hosts and external:
|
||||
if not host and len(route.ctx.hosts) > 1:
|
||||
if route.extra.hosts and external:
|
||||
if not host and len(route.extra.hosts) > 1:
|
||||
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(
|
||||
f"Requested host ({host}) is not available for this "
|
||||
f"route: {route.ctx.hosts}"
|
||||
f"route: {route.extra.hosts}"
|
||||
)
|
||||
elif not host:
|
||||
host = list(route.ctx.hosts)[0]
|
||||
host = list(route.extra.hosts)[0]
|
||||
|
||||
if scheme and not external:
|
||||
raise ValueError("When specifying _scheme, _external must be True")
|
||||
@@ -708,10 +713,276 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||
exception: BaseException,
|
||||
run_middleware: bool = True,
|
||||
): # 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
|
||||
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(
|
||||
self, handler, request, *args, subprotocols=None, **kwargs
|
||||
@@ -1074,18 +1345,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||
def debug(self):
|
||||
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
|
||||
def auto_reload(self):
|
||||
return self.config.AUTO_RELOAD
|
||||
@@ -1102,58 +1361,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||
"""
|
||||
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
|
||||
def reload_dirs(self):
|
||||
return self.state.reload_dirs
|
||||
@@ -1248,6 +1455,16 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||
return cls(name)
|
||||
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
|
||||
# -------------------------------------------------------------------- #
|
||||
@@ -1297,17 +1514,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
||||
23.3,
|
||||
)
|
||||
|
||||
# TODO: Replace in v22.6 to check against apps in app registry
|
||||
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
|
||||
Sanic._check_uvloop_conflict()
|
||||
|
||||
# Startup time optimizations
|
||||
if self.state.primary:
|
||||
|
||||
@@ -7,7 +7,6 @@ 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
|
||||
@@ -231,9 +230,11 @@ class ASGIApp:
|
||||
"""
|
||||
Handle the incoming request.
|
||||
"""
|
||||
manager = RequestManager.create(self.request)
|
||||
try:
|
||||
self.stage = Stage.HANDLER
|
||||
await manager.handle()
|
||||
await self.sanic_app.handle_request(self.request)
|
||||
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.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.exceptions += exception_handlers
|
||||
|
||||
@@ -12,7 +12,7 @@ from sanic.constants import LocalCertCreator
|
||||
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
|
||||
from sanic.helpers import Default, _default
|
||||
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
|
||||
|
||||
|
||||
@@ -71,10 +71,6 @@ DEFAULT_CONFIG = {
|
||||
"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):
|
||||
def __init__(cls, *_):
|
||||
@@ -132,6 +128,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
):
|
||||
defaults = defaults or {}
|
||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||
self._configure_warnings()
|
||||
|
||||
self._converters = [str, str_to_bool, float, int]
|
||||
|
||||
@@ -149,7 +146,6 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
self.load_environment_vars(SANIC_PREFIX)
|
||||
|
||||
self._configure_header_size()
|
||||
self._configure_warnings()
|
||||
self._check_error_format()
|
||||
self._init = True
|
||||
|
||||
@@ -241,7 +237,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
"""
|
||||
Looks for prefixed environment variables and applies them to the
|
||||
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:
|
||||
|
||||
@@ -267,12 +265,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
`See user guide re: config
|
||||
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
||||
"""
|
||||
lower_case_var_found = False
|
||||
for key, value in environ.items():
|
||||
if not key.startswith(prefix):
|
||||
if not key.startswith(prefix) or not key.isupper():
|
||||
continue
|
||||
if not key.isupper():
|
||||
lower_case_var_found = True
|
||||
|
||||
_, config_key = key.split(prefix, 1)
|
||||
|
||||
@@ -282,12 +277,6 @@ class Config(dict, metaclass=DescriptorMeta):
|
||||
break
|
||||
except ValueError:
|
||||
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]):
|
||||
"""
|
||||
|
||||
@@ -448,8 +448,8 @@ def exception_response(
|
||||
# from the route
|
||||
if request.route:
|
||||
try:
|
||||
if request.route.ctx.error_format:
|
||||
render_format = request.route.ctx.error_format
|
||||
if request.route.extra.error_format:
|
||||
render_format = request.route.extra.error_format
|
||||
except AttributeError:
|
||||
...
|
||||
|
||||
|
||||
@@ -1,317 +1,16 @@
|
||||
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.http.constants import Stage
|
||||
from sanic.log import deprecation, error_logger, logger
|
||||
from sanic.log import deprecation, error_logger
|
||||
from sanic.models.handler_types import RouteHandler
|
||||
from sanic.request import Request
|
||||
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(_):
|
||||
...
|
||||
from sanic.response import text
|
||||
|
||||
|
||||
class ErrorHandler:
|
||||
|
||||
@@ -124,8 +124,7 @@ class Http(Stream, metaclass=TouchUpMeta):
|
||||
|
||||
self.stage = Stage.HANDLER
|
||||
self.request.conn_info = self.protocol.conn_info
|
||||
|
||||
await self.request.manager.handle()
|
||||
await self.protocol.request_handler(self.request)
|
||||
|
||||
# Handler finished, response should've been sent
|
||||
if self.stage is Stage.HANDLER and not self.upgrade_websocket:
|
||||
@@ -251,7 +250,6 @@ 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",
|
||||
@@ -425,11 +423,15 @@ 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 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:
|
||||
"""
|
||||
|
||||
12
sanic/log.py
12
sanic/log.py
@@ -25,6 +25,12 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
||||
"propagate": True,
|
||||
"qualname": "sanic.access",
|
||||
},
|
||||
"sanic.server": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"],
|
||||
"propagate": True,
|
||||
"qualname": "sanic.server",
|
||||
},
|
||||
},
|
||||
handlers={
|
||||
"console": {
|
||||
@@ -101,6 +107,12 @@ Logger used by Sanic for access logging
|
||||
"""
|
||||
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
|
||||
version_info = f"[DEPRECATION v{version}] "
|
||||
|
||||
@@ -21,7 +21,7 @@ class Middleware:
|
||||
def __init__(
|
||||
self,
|
||||
func: MiddlewareType,
|
||||
location: MiddlewareLocation = MiddlewareLocation.REQUEST,
|
||||
location: MiddlewareLocation,
|
||||
priority: int = 0,
|
||||
) -> None:
|
||||
self.func = func
|
||||
@@ -33,10 +33,9 @@ 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 {name}>, "
|
||||
f"func=<function {self.func.__name__}>, "
|
||||
f"priority={self.priority}, "
|
||||
f"location={self.location.name})"
|
||||
)
|
||||
@@ -64,3 +63,4 @@ class Middleware:
|
||||
@classmethod
|
||||
def reset_count(cls):
|
||||
cls._counter = count()
|
||||
cls.count = next(cls._counter)
|
||||
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
@@ -105,23 +104,19 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
||||
self.named_response_middleware.get(route.name, deque()),
|
||||
location=MiddlewareLocation.RESPONSE,
|
||||
)
|
||||
|
||||
route.handler = RequestHandler(
|
||||
route.handler,
|
||||
deque(
|
||||
route.extra.request_middleware = deque(
|
||||
sorted(
|
||||
request_middleware,
|
||||
key=attrgetter("order"),
|
||||
reverse=True,
|
||||
)
|
||||
),
|
||||
deque(
|
||||
)
|
||||
route.extra.response_middleware = deque(
|
||||
sorted(
|
||||
response_middleware,
|
||||
key=attrgetter("order"),
|
||||
reverse=True,
|
||||
)[::-1]
|
||||
),
|
||||
)
|
||||
request_middleware = Middleware.convert(
|
||||
self.request_middleware,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
from ast import NodeVisitor, Return, parse
|
||||
from contextlib import suppress
|
||||
from email.utils import formatdate
|
||||
from functools import partial, wraps
|
||||
from inspect import getsource, signature
|
||||
from mimetypes import guess_type
|
||||
from os import path
|
||||
from pathlib import Path, PurePath
|
||||
from textwrap import dedent
|
||||
from time import gmtime, strftime
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
@@ -31,21 +32,13 @@ from sanic.handlers import ContentRangeHandler
|
||||
from sanic.log import error_logger
|
||||
from sanic.models.futures import FutureRoute, FutureStatic
|
||||
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
|
||||
|
||||
|
||||
RouteWrapper = Callable[
|
||||
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
||||
]
|
||||
RESTRICTED_ROUTE_CONTEXT = (
|
||||
"ignore_body",
|
||||
"stream",
|
||||
"hosts",
|
||||
"static",
|
||||
"error_format",
|
||||
"websocket",
|
||||
)
|
||||
|
||||
|
||||
class RouteMixin(metaclass=SanicMeta):
|
||||
@@ -790,24 +783,9 @@ class RouteMixin(metaclass=SanicMeta):
|
||||
|
||||
return name
|
||||
|
||||
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,
|
||||
):
|
||||
# Merge served directory and requested file if provided
|
||||
async def _get_file_path(self, file_or_directory, __file_uri__, not_found):
|
||||
file_path_raw = Path(unquote(file_or_directory))
|
||||
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__:
|
||||
# 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__}"
|
||||
)
|
||||
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:
|
||||
headers = {}
|
||||
# Check if the client has been sent this file before
|
||||
@@ -841,15 +842,13 @@ class RouteMixin(metaclass=SanicMeta):
|
||||
stats = None
|
||||
if use_modified_since:
|
||||
stats = await stat_async(file_path)
|
||||
modified_since = strftime(
|
||||
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
|
||||
modified_since = 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
|
||||
if use_content_range:
|
||||
_range = None
|
||||
@@ -864,8 +863,7 @@ class RouteMixin(metaclass=SanicMeta):
|
||||
pass
|
||||
else:
|
||||
del headers["Content-Length"]
|
||||
for key, value in _range.headers.items():
|
||||
headers[key] = value
|
||||
headers.update(_range.headers)
|
||||
|
||||
if "content-type" not in headers:
|
||||
content_type = (
|
||||
@@ -1041,24 +1039,12 @@ class RouteMixin(metaclass=SanicMeta):
|
||||
|
||||
return types
|
||||
|
||||
def _build_route_context(self, raw):
|
||||
def _build_route_context(self, raw: Dict[str, Any]) -> HashableDict:
|
||||
ctx_kwargs = {
|
||||
key.replace("ctx_", ""): raw.pop(key)
|
||||
for key in {**raw}.keys()
|
||||
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:
|
||||
unexpected_arguments = ", ".join(raw.keys())
|
||||
raise TypeError(
|
||||
|
||||
@@ -35,6 +35,7 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from sanic.application.ext import setup_ext
|
||||
from sanic.application.logo import get_logo
|
||||
from sanic.application.motd import MOTD
|
||||
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
|
||||
@@ -558,7 +559,6 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
|
||||
def motd(
|
||||
self,
|
||||
serve_location: str = "",
|
||||
server_settings: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
if (
|
||||
@@ -568,13 +568,6 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
or os.environ.get("SANIC_SERVER_RUNNING")
|
||||
):
|
||||
return
|
||||
if serve_location:
|
||||
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:
|
||||
logo = get_logo(coffee=self.state.coffee)
|
||||
@@ -746,9 +739,12 @@ class StartupMixin(metaclass=SanicMeta):
|
||||
|
||||
socks = []
|
||||
sync_manager = Manager()
|
||||
setup_ext(primary)
|
||||
try:
|
||||
main_start = primary_server_info.settings.pop("main_start", None)
|
||||
main_stop = primary_server_info.settings.pop("main_stop", None)
|
||||
primary_server_info.settings.pop("main_start", 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.setup_loop()
|
||||
loop = new_event_loop()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextvars import ContextVar
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -24,7 +23,6 @@ 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
|
||||
|
||||
@@ -39,7 +37,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 Header
|
||||
from sanic.compat import CancelledErrors, Header
|
||||
from sanic.constants import (
|
||||
CACHEABLE_HTTP_METHODS,
|
||||
DEFAULT_HTTP_CONTENT_TYPE,
|
||||
@@ -101,7 +99,6 @@ class Request:
|
||||
"_cookies",
|
||||
"_id",
|
||||
"_ip",
|
||||
"_manager",
|
||||
"_parsed_url",
|
||||
"_port",
|
||||
"_protocol",
|
||||
@@ -185,7 +182,6 @@ 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
|
||||
@@ -229,7 +225,7 @@ class Request:
|
||||
"Request.request_middleware_started has been deprecated and will"
|
||||
"be removed. You should set a flag on the request context using"
|
||||
"either middleware or signals if you need this feature.",
|
||||
22.3,
|
||||
23.3,
|
||||
)
|
||||
return self._request_middleware_started
|
||||
|
||||
@@ -247,10 +243,6 @@ class Request:
|
||||
)
|
||||
return self._stream_id
|
||||
|
||||
@property
|
||||
def manager(self):
|
||||
return self._manager
|
||||
|
||||
def reset_response(self):
|
||||
try:
|
||||
if (
|
||||
@@ -341,13 +333,19 @@ class Request:
|
||||
if isawaitable(response):
|
||||
response = await response # type: ignore
|
||||
# Run response middleware
|
||||
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)
|
||||
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"
|
||||
)
|
||||
self.responded = True
|
||||
return response
|
||||
|
||||
@@ -13,7 +13,6 @@ from sanic_routing.route import Route
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.errorpages import check_error_format
|
||||
from sanic.exceptions import MethodNotAllowed, NotFound, SanicException
|
||||
from sanic.handlers import RequestHandler
|
||||
from sanic.models.handler_types import RouteHandler
|
||||
|
||||
|
||||
@@ -32,11 +31,9 @@ class Router(BaseRouter):
|
||||
|
||||
def _get(
|
||||
self, path: str, method: str, host: Optional[str]
|
||||
) -> Tuple[Route, RequestHandler, Dict[str, Any]]:
|
||||
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
|
||||
try:
|
||||
# We know this will always be RequestHandler, so we can ignore
|
||||
# typing issue here
|
||||
return self.resolve( # type: ignore
|
||||
return self.resolve(
|
||||
path=path,
|
||||
method=method,
|
||||
extra={"host": host} if host else None,
|
||||
@@ -53,7 +50,7 @@ class Router(BaseRouter):
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def get( # type: ignore
|
||||
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
|
||||
a response for a given request
|
||||
@@ -62,7 +59,7 @@ class Router(BaseRouter):
|
||||
:type request: Request
|
||||
:return: details needed for handling the request and returning the
|
||||
correct response
|
||||
:rtype: Tuple[ Route, RequestHandler, Dict[str, Any]]
|
||||
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
|
||||
"""
|
||||
return self._get(path, method, host)
|
||||
|
||||
@@ -117,7 +114,7 @@ class Router(BaseRouter):
|
||||
|
||||
params = dict(
|
||||
path=uri,
|
||||
handler=RequestHandler(handler, [], []),
|
||||
handler=handler,
|
||||
methods=frozenset(map(str, methods)) if methods else None,
|
||||
name=name,
|
||||
strict=strict_slashes,
|
||||
@@ -136,14 +133,14 @@ class Router(BaseRouter):
|
||||
params.update({"requirements": {"host": host}})
|
||||
|
||||
route = super().add(**params) # type: ignore
|
||||
route.ctx.ignore_body = ignore_body
|
||||
route.ctx.stream = stream
|
||||
route.ctx.hosts = hosts
|
||||
route.ctx.static = static
|
||||
route.ctx.error_format = error_format
|
||||
route.extra.ignore_body = ignore_body
|
||||
route.extra.stream = stream
|
||||
route.extra.hosts = hosts
|
||||
route.extra.static = static
|
||||
route.extra.error_format = error_format
|
||||
|
||||
if error_format:
|
||||
check_error_format(route.ctx.error_format)
|
||||
check_error_format(route.extra.error_format)
|
||||
|
||||
routes.append(route)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ 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
|
||||
@@ -58,7 +57,7 @@ class HttpProtocolMixin:
|
||||
def _setup(self):
|
||||
self.request: Optional[Request] = None
|
||||
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.request_timeout = self.app.config.REQUEST_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.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
||||
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.server.async_server import AsyncioServer
|
||||
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):
|
||||
pid = os.getpid()
|
||||
try:
|
||||
logger.info("Starting worker [%s]", pid)
|
||||
server_logger.info("Starting worker [%s]", pid)
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
logger.info("Stopping worker [%s]", pid)
|
||||
server_logger.info("Stopping worker [%s]", pid)
|
||||
|
||||
loop.run_until_complete(before_stop())
|
||||
|
||||
@@ -372,7 +372,9 @@ def serve_multiple(server_settings, workers):
|
||||
processes = []
|
||||
|
||||
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:
|
||||
os.kill(process.pid, SIGTERM)
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ from typing import (
|
||||
)
|
||||
|
||||
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.server import ServerConnection
|
||||
from websockets.typing import Data
|
||||
@@ -840,3 +844,10 @@ class WebsocketImplProtocol:
|
||||
self.abort_pings()
|
||||
if self.connection_lost_waiter:
|
||||
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):
|
||||
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
|
||||
|
||||
def reload(self):
|
||||
@@ -84,10 +84,11 @@ class Inspector:
|
||||
message = "__TERMINATE__"
|
||||
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():
|
||||
if isinstance(value, dict):
|
||||
obj[key] = self._make_safe(value)
|
||||
obj[key] = Inspector.make_safe(value)
|
||||
elif isinstance(value, datetime):
|
||||
obj[key] = value.isoformat()
|
||||
return obj
|
||||
|
||||
2
setup.py
2
setup.py
@@ -94,7 +94,7 @@ requirements = [
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
"sanic-testing>=22.9.0b1",
|
||||
"sanic-testing>=22.9.0b2",
|
||||
"pytest",
|
||||
"coverage",
|
||||
"beautifulsoup4",
|
||||
|
||||
@@ -18,6 +18,7 @@ from sanic.exceptions import SanicException
|
||||
from sanic.helpers import _default
|
||||
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
||||
from sanic.response import text
|
||||
from sanic.router import Route
|
||||
|
||||
|
||||
@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):
|
||||
mock = Mock()
|
||||
mock.handler = None
|
||||
app.config.TOUCHUP = False
|
||||
route = Mock(spec=Route)
|
||||
route.extra.request_middleware = []
|
||||
route.extra.response_middleware = []
|
||||
|
||||
def mockreturn(*args, **kwargs):
|
||||
return mock, None, {}
|
||||
return route, None, {}
|
||||
|
||||
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])
|
||||
|
||||
assert counter[(logging.WARNING, message)] == 2
|
||||
assert counter[(logging.WARNING, message)] == 3
|
||||
|
||||
|
||||
def test_cannot_run_fast_and_workers(app: Sanic):
|
||||
|
||||
@@ -125,14 +125,9 @@ def test_env_w_custom_converter():
|
||||
|
||||
|
||||
def test_env_lowercase():
|
||||
with pytest.warns(None) as record:
|
||||
environ["SANIC_test_answer"] = "42"
|
||||
app = Sanic(name="Test")
|
||||
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."
|
||||
)
|
||||
assert "test_answer" not in app.config
|
||||
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):
|
||||
@app.get("/text")
|
||||
def text_response(request):
|
||||
return text(request.route.ctx.error_format)
|
||||
return text(request.route.extra.error_format)
|
||||
|
||||
@app.get("/json")
|
||||
def json_response(request):
|
||||
return json({"format": request.route.ctx.error_format})
|
||||
return json({"format": request.route.extra.error_format})
|
||||
|
||||
@app.get("/html")
|
||||
def html_response(request):
|
||||
return html(request.route.ctx.error_format)
|
||||
return html(request.route.extra.error_format)
|
||||
|
||||
_, response = app.test_client.get("/text")
|
||||
assert response.text == "text"
|
||||
|
||||
@@ -2,22 +2,12 @@ import logging
|
||||
|
||||
from asyncio import CancelledError
|
||||
from itertools import count
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic.exceptions import NotFound
|
||||
from sanic.middleware import Middleware
|
||||
from sanic.request import Request
|
||||
from sanic.response import HTTPResponse, json, text
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_middleware():
|
||||
yield
|
||||
Middleware.reset_count()
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# GET
|
||||
# ------------------------------------------------------------ #
|
||||
@@ -193,7 +183,7 @@ def test_middleware_response_raise_exception(app, caplog):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
reqrequest, response = app.test_client.get("/fail")
|
||||
|
||||
assert response.status == 500
|
||||
assert response.status == 404
|
||||
# 404 errors are not logged
|
||||
assert (
|
||||
"sanic.error",
|
||||
@@ -328,15 +318,6 @@ def test_middleware_return_response(app):
|
||||
resp1 = await request.respond()
|
||||
return resp1
|
||||
|
||||
app.test_client.get("/")
|
||||
_, response = app.test_client.get("/")
|
||||
assert response_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
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.middleware import Middleware
|
||||
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(
|
||||
"expected,priorities",
|
||||
PRIORITY_TEST_CASES,
|
||||
|
||||
@@ -4,8 +4,8 @@ import os
|
||||
import time
|
||||
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from email.utils import formatdate
|
||||
from datetime import datetime, timedelta
|
||||
from email.utils import formatdate, parsedate_to_datetime
|
||||
from logging import ERROR, LogRecord
|
||||
from mimetypes import guess_type
|
||||
from pathlib import Path
|
||||
@@ -665,13 +665,11 @@ def test_multiple_responses(
|
||||
|
||||
with caplog.at_level(ERROR):
|
||||
_, response = app.test_client.get("/4")
|
||||
print(response.json)
|
||||
assert response.status == 200
|
||||
assert "foo" not in response.text
|
||||
assert "one" in response.headers
|
||||
assert response.headers["one"] == "one"
|
||||
|
||||
print(response.headers)
|
||||
assert message_in_records(caplog.records, error_msg2)
|
||||
|
||||
with caplog.at_level(ERROR):
|
||||
@@ -841,10 +839,10 @@ def test_file_validate(app: Sanic, static_file_directory: str):
|
||||
time.sleep(1)
|
||||
with open(file_path, "a") as f:
|
||||
f.write("bar\n")
|
||||
|
||||
_, response = app.test_client.get(
|
||||
"/validate", headers={"If-Modified-Since": last_modified}
|
||||
)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.body == b"foo\nbar\n"
|
||||
|
||||
@@ -921,3 +919,28 @@ def test_file_validating_304_response(
|
||||
)
|
||||
assert response.status == 304
|
||||
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])
|
||||
|
||||
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.error", logging.ERROR)] == 0
|
||||
assert counter[("sanic.server", logging.INFO)] == 2
|
||||
|
||||
|
||||
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])
|
||||
|
||||
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.error", logging.ERROR)] == 0
|
||||
assert counter[("sanic.server", logging.INFO)] == 2
|
||||
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