Compare commits

..

7 Commits

Author SHA1 Message Date
Adam Hopkins
5052321801 Remove deprecated items (#2555) 2022-09-29 01:07:09 +03:00
Adam Hopkins
23ce4eaaa4 Merge branch 'main' of github.com:sanic-org/sanic 2022-09-23 00:16:27 +03:00
Adam Hopkins
23a430c4ad Set version properly 2022-09-23 00:16:10 +03:00
Adam Hopkins
ec158ffa69 Additional logger and support for multiprocess manager (#2551) 2022-09-23 00:01:33 +03:00
Adam Hopkins
6e32270036 Begin middleware revamp (#2550) 2022-09-22 00:43:42 +03:00
Zhiwei
43ba381e7b Refactor _static_request_handler (#2533)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-09-21 00:45:03 +03:00
Zhiwei
16503319e5 Make WebsocketImplProtocol async iterable (#2490) 2022-09-21 00:20:32 +03:00
28 changed files with 551 additions and 589 deletions

View File

@@ -1 +1 @@
__version__ = "22.9.1"
__version__ = "22.9.0"

View File

@@ -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:

View File

@@ -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)

View File

@@ -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

View File

@@ -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]):
"""

View File

@@ -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:
...

View File

@@ -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:

View File

@@ -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:
"""

View File

@@ -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}] "

View File

@@ -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)

View File

@@ -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,

View File

@@ -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(

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -94,7 +94,7 @@ requirements = [
]
tests_require = [
"sanic-testing>=22.9.0b1",
"sanic-testing>=22.9.0b2",
"pytest",
"coverage",
"beautifulsoup4",

View File

@@ -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):

View File

@@ -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"]

View File

@@ -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"

View File

@@ -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)

View File

@@ -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,

View File

@@ -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""

View File

@@ -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
View 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"]