Merge branch 'main' into zhiwei/route-overwrite
This commit is contained in:
commit
e5696342f8
@ -2,6 +2,22 @@ from sanic.__version__ import __version__
|
|||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.constants import HTTPMethod
|
from sanic.constants import HTTPMethod
|
||||||
|
from sanic.exceptions import (
|
||||||
|
BadRequest,
|
||||||
|
ExpectationFailed,
|
||||||
|
FileNotFound,
|
||||||
|
Forbidden,
|
||||||
|
HeaderNotFound,
|
||||||
|
InternalServerError,
|
||||||
|
InvalidHeader,
|
||||||
|
MethodNotAllowed,
|
||||||
|
NotFound,
|
||||||
|
RangeNotSatisfiable,
|
||||||
|
SanicException,
|
||||||
|
ServerError,
|
||||||
|
ServiceUnavailable,
|
||||||
|
Unauthorized,
|
||||||
|
)
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import (
|
from sanic.response import (
|
||||||
HTTPResponse,
|
HTTPResponse,
|
||||||
@ -9,6 +25,7 @@ from sanic.response import (
|
|||||||
file,
|
file,
|
||||||
html,
|
html,
|
||||||
json,
|
json,
|
||||||
|
raw,
|
||||||
redirect,
|
redirect,
|
||||||
text,
|
text,
|
||||||
)
|
)
|
||||||
@ -17,16 +34,34 @@ from sanic.server.websockets.impl import WebsocketImplProtocol as Websocket
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"__version__",
|
"__version__",
|
||||||
|
# Common objects
|
||||||
"Sanic",
|
"Sanic",
|
||||||
"Blueprint",
|
"Blueprint",
|
||||||
"HTTPMethod",
|
"HTTPMethod",
|
||||||
"HTTPResponse",
|
"HTTPResponse",
|
||||||
"Request",
|
"Request",
|
||||||
"Websocket",
|
"Websocket",
|
||||||
|
# Common exceptions
|
||||||
|
"BadRequest",
|
||||||
|
"ExpectationFailed",
|
||||||
|
"FileNotFound",
|
||||||
|
"Forbidden",
|
||||||
|
"HeaderNotFound",
|
||||||
|
"InternalServerError",
|
||||||
|
"InvalidHeader",
|
||||||
|
"MethodNotAllowed",
|
||||||
|
"NotFound",
|
||||||
|
"RangeNotSatisfiable",
|
||||||
|
"SanicException",
|
||||||
|
"ServerError",
|
||||||
|
"ServiceUnavailable",
|
||||||
|
"Unauthorized",
|
||||||
|
# Common response methods
|
||||||
"empty",
|
"empty",
|
||||||
"file",
|
"file",
|
||||||
"html",
|
"html",
|
||||||
"json",
|
"json",
|
||||||
|
"raw",
|
||||||
"redirect",
|
"redirect",
|
||||||
"text",
|
"text",
|
||||||
)
|
)
|
||||||
|
33
sanic/app.py
33
sanic/app.py
@ -16,7 +16,7 @@ from asyncio import (
|
|||||||
)
|
)
|
||||||
from asyncio.futures import Future
|
from asyncio.futures import Future
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from contextlib import suppress
|
from contextlib import contextmanager, suppress
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from os import environ
|
from os import environ
|
||||||
@ -33,6 +33,7 @@ from typing import (
|
|||||||
Deque,
|
Deque,
|
||||||
Dict,
|
Dict,
|
||||||
Iterable,
|
Iterable,
|
||||||
|
Iterator,
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
Set,
|
Set,
|
||||||
@ -91,6 +92,7 @@ from sanic.signals import Signal, SignalRouter
|
|||||||
from sanic.touchup import TouchUp, TouchUpMeta
|
from sanic.touchup import TouchUp, TouchUpMeta
|
||||||
from sanic.types.shared_ctx import SharedContext
|
from sanic.types.shared_ctx import SharedContext
|
||||||
from sanic.worker.inspector import Inspector
|
from sanic.worker.inspector import Inspector
|
||||||
|
from sanic.worker.loader import CertLoader
|
||||||
from sanic.worker.manager import WorkerManager
|
from sanic.worker.manager import WorkerManager
|
||||||
|
|
||||||
|
|
||||||
@ -138,6 +140,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
"_test_client",
|
"_test_client",
|
||||||
"_test_manager",
|
"_test_manager",
|
||||||
"blueprints",
|
"blueprints",
|
||||||
|
"certloader_class",
|
||||||
"config",
|
"config",
|
||||||
"configure_logging",
|
"configure_logging",
|
||||||
"ctx",
|
"ctx",
|
||||||
@ -180,6 +183,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
loads: Optional[Callable[..., Any]] = None,
|
loads: Optional[Callable[..., Any]] = None,
|
||||||
inspector: bool = False,
|
inspector: bool = False,
|
||||||
inspector_class: Optional[Type[Inspector]] = None,
|
inspector_class: Optional[Type[Inspector]] = None,
|
||||||
|
certloader_class: Optional[Type[CertLoader]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(name=name)
|
super().__init__(name=name)
|
||||||
# logging
|
# logging
|
||||||
@ -214,6 +218,9 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
self.asgi = False
|
self.asgi = False
|
||||||
self.auto_reload = False
|
self.auto_reload = False
|
||||||
self.blueprints: Dict[str, Blueprint] = {}
|
self.blueprints: Dict[str, Blueprint] = {}
|
||||||
|
self.certloader_class: Type[CertLoader] = (
|
||||||
|
certloader_class or CertLoader
|
||||||
|
)
|
||||||
self.configure_logging: bool = configure_logging
|
self.configure_logging: bool = configure_logging
|
||||||
self.ctx: Any = ctx or SimpleNamespace()
|
self.ctx: Any = ctx or SimpleNamespace()
|
||||||
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
|
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
|
||||||
@ -436,6 +443,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
ctx = params.pop("route_context")
|
ctx = params.pop("route_context")
|
||||||
|
|
||||||
|
with self.amend():
|
||||||
routes = self.router.add(**params)
|
routes = self.router.add(**params)
|
||||||
if isinstance(routes, Route):
|
if isinstance(routes, Route):
|
||||||
routes = [routes]
|
routes = [routes]
|
||||||
@ -452,6 +460,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
middleware: FutureMiddleware,
|
middleware: FutureMiddleware,
|
||||||
route_names: Optional[List[str]] = None,
|
route_names: Optional[List[str]] = None,
|
||||||
):
|
):
|
||||||
|
with self.amend():
|
||||||
if route_names:
|
if route_names:
|
||||||
return self.register_named_middleware(
|
return self.register_named_middleware(
|
||||||
middleware.middleware, route_names, middleware.attach_to
|
middleware.middleware, route_names, middleware.attach_to
|
||||||
@ -462,6 +471,7 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _apply_signal(self, signal: FutureSignal) -> Signal:
|
def _apply_signal(self, signal: FutureSignal) -> Signal:
|
||||||
|
with self.amend():
|
||||||
return self.signal_router.add(*signal)
|
return self.signal_router.add(*signal)
|
||||||
|
|
||||||
def dispatch(
|
def dispatch(
|
||||||
@ -1523,6 +1533,27 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
# Lifecycle
|
# Lifecycle
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def amend(self) -> Iterator[None]:
|
||||||
|
"""
|
||||||
|
If the application has started, this function allows changes
|
||||||
|
to be made to add routes, middleware, and signals.
|
||||||
|
"""
|
||||||
|
if not self.state.is_started:
|
||||||
|
yield
|
||||||
|
else:
|
||||||
|
do_router = self.router.finalized
|
||||||
|
do_signal_router = self.signal_router.finalized
|
||||||
|
if do_router:
|
||||||
|
self.router.reset()
|
||||||
|
if do_signal_router:
|
||||||
|
self.signal_router.reset()
|
||||||
|
yield
|
||||||
|
if do_signal_router:
|
||||||
|
self.signalize(self.config.TOUCHUP)
|
||||||
|
if do_router:
|
||||||
|
self.finalize()
|
||||||
|
|
||||||
def finalize(self):
|
def finalize(self):
|
||||||
try:
|
try:
|
||||||
self.router.finalize()
|
self.router.finalize()
|
||||||
|
@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Optional
|
|||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from sanic.compat import Header
|
from sanic.compat import Header
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import BadRequest, ServerError
|
||||||
from sanic.helpers import Default
|
from sanic.helpers import Default
|
||||||
from sanic.http import Stage
|
from sanic.http import Stage
|
||||||
from sanic.log import error_logger, logger
|
from sanic.log import error_logger, logger
|
||||||
@ -132,12 +132,20 @@ class ASGIApp:
|
|||||||
instance.sanic_app.state.is_started = True
|
instance.sanic_app.state.is_started = True
|
||||||
setattr(instance.transport, "add_task", sanic_app.loop.create_task)
|
setattr(instance.transport, "add_task", sanic_app.loop.create_task)
|
||||||
|
|
||||||
|
try:
|
||||||
headers = Header(
|
headers = Header(
|
||||||
[
|
[
|
||||||
(key.decode("latin-1"), value.decode("latin-1"))
|
(
|
||||||
|
key.decode("ASCII"),
|
||||||
|
value.decode(errors="surrogateescape"),
|
||||||
|
)
|
||||||
for key, value in scope.get("headers", [])
|
for key, value in scope.get("headers", [])
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise BadRequest(
|
||||||
|
"Header names can only contain US-ASCII characters"
|
||||||
|
)
|
||||||
path = (
|
path = (
|
||||||
scope["path"][1:]
|
scope["path"][1:]
|
||||||
if scope["path"].startswith("/")
|
if scope["path"].startswith("/")
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from asyncio import CancelledError
|
from asyncio import CancelledError, Protocol
|
||||||
from typing import Any, Dict, Optional, Union
|
from os import PathLike
|
||||||
|
from typing import Any, Dict, Optional, Sequence, Union
|
||||||
|
|
||||||
from sanic.helpers import STATUS_CODES
|
from sanic.helpers import STATUS_CODES
|
||||||
|
|
||||||
@ -9,51 +10,158 @@ class RequestCancelled(CancelledError):
|
|||||||
|
|
||||||
|
|
||||||
class ServerKilled(Exception):
|
class ServerKilled(Exception):
|
||||||
...
|
"""
|
||||||
|
Exception Sanic server uses when killing a server process for something
|
||||||
|
unexpected happening.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class SanicException(Exception):
|
class SanicException(Exception):
|
||||||
|
"""
|
||||||
|
Generic exception that will generate an HTTP response when raised
|
||||||
|
in the context of a request lifecycle.
|
||||||
|
|
||||||
|
Usually it is best practice to use one of the more specific exceptions
|
||||||
|
than this generic. Even when trying to raise a 500, it is generally
|
||||||
|
preferrable to use :class:`.ServerError`
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
raise SanicException(
|
||||||
|
"Something went wrong",
|
||||||
|
status_code=999,
|
||||||
|
context={
|
||||||
|
"info": "Some additional details",
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
"X-Foo": "bar"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the appropriate HTTP response status message will be used
|
||||||
|
instead, defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param status_code: The HTTP response code to send, if applicable. If
|
||||||
|
``None``, then it will be 500, defaults to None
|
||||||
|
:type status_code: Optional[int], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
|
"""
|
||||||
|
|
||||||
|
status_code: int = 500
|
||||||
|
quiet: Optional[bool] = False
|
||||||
|
headers: Dict[str, str] = {}
|
||||||
message: str = ""
|
message: str = ""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
message: Optional[Union[str, bytes]] = None,
|
message: Optional[Union[str, bytes]] = None,
|
||||||
status_code: Optional[int] = None,
|
status_code: Optional[int] = None,
|
||||||
|
*,
|
||||||
quiet: Optional[bool] = None,
|
quiet: Optional[bool] = None,
|
||||||
context: Optional[Dict[str, Any]] = None,
|
context: Optional[Dict[str, Any]] = None,
|
||||||
extra: Optional[Dict[str, Any]] = None,
|
extra: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, Any]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
self.extra = extra
|
self.extra = extra
|
||||||
|
status_code = status_code or getattr(
|
||||||
|
self.__class__, "status_code", None
|
||||||
|
)
|
||||||
|
quiet = quiet or getattr(self.__class__, "quiet", None)
|
||||||
|
headers = headers or getattr(self.__class__, "headers", {})
|
||||||
if message is None:
|
if message is None:
|
||||||
if self.message:
|
if self.message:
|
||||||
message = self.message
|
message = self.message
|
||||||
elif status_code is not None:
|
elif status_code:
|
||||||
msg: bytes = STATUS_CODES.get(status_code, b"")
|
msg: bytes = STATUS_CODES.get(status_code, b"")
|
||||||
message = msg.decode("utf8")
|
message = msg.decode("utf8")
|
||||||
|
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
||||||
if status_code is not None:
|
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
|
self.quiet = quiet
|
||||||
# quiet=None/False/True with None meaning choose by status
|
self.headers = headers
|
||||||
if quiet or quiet is None and status_code not in (None, 500):
|
|
||||||
self.quiet = True
|
|
||||||
|
|
||||||
|
|
||||||
class NotFound(SanicException):
|
class HTTPException(SanicException):
|
||||||
|
"""
|
||||||
|
A base class for other exceptions and should not be called directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: Optional[Union[str, bytes]] = None,
|
||||||
|
*,
|
||||||
|
quiet: Optional[bool] = None,
|
||||||
|
context: Optional[Dict[str, Any]] = None,
|
||||||
|
extra: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
message,
|
||||||
|
quiet=quiet,
|
||||||
|
context=context,
|
||||||
|
extra=extra,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(HTTPException):
|
||||||
"""
|
"""
|
||||||
**Status**: 404 Not Found
|
**Status**: 404 Not Found
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Not Found' will be sent, defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 404
|
status_code = 404
|
||||||
quiet = True
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class BadRequest(SanicException):
|
class BadRequest(HTTPException):
|
||||||
"""
|
"""
|
||||||
**Status**: 400 Bad Request
|
**Status**: 400 Bad Request
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Bad Request' will be sent, defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 400
|
status_code = 400
|
||||||
@ -61,51 +169,133 @@ class BadRequest(SanicException):
|
|||||||
|
|
||||||
|
|
||||||
InvalidUsage = BadRequest
|
InvalidUsage = BadRequest
|
||||||
|
BadURL = BadRequest
|
||||||
|
|
||||||
|
|
||||||
class BadURL(BadRequest):
|
class MethodNotAllowed(HTTPException):
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class MethodNotAllowed(SanicException):
|
|
||||||
"""
|
"""
|
||||||
**Status**: 405 Method Not Allowed
|
**Status**: 405 Method Not Allowed
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Method Not Allowed' will be sent,
|
||||||
|
defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param method: The HTTP method that was used, defaults to an empty string
|
||||||
|
:type method: Optional[str], optional
|
||||||
|
:param allowed_methods: The HTTP methods that can be used instead of the
|
||||||
|
one that was attempted
|
||||||
|
:type allowed_methods: Optional[Sequence[str]], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 405
|
status_code = 405
|
||||||
quiet = True
|
quiet = True
|
||||||
|
|
||||||
def __init__(self, message, method, allowed_methods):
|
def __init__(
|
||||||
super().__init__(message)
|
self,
|
||||||
self.headers = {"Allow": ", ".join(allowed_methods)}
|
message: Optional[Union[str, bytes]] = None,
|
||||||
|
method: str = "",
|
||||||
|
allowed_methods: Optional[Sequence[str]] = None,
|
||||||
|
*,
|
||||||
|
quiet: Optional[bool] = None,
|
||||||
|
context: Optional[Dict[str, Any]] = None,
|
||||||
|
extra: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
message,
|
||||||
|
quiet=quiet,
|
||||||
|
context=context,
|
||||||
|
extra=extra,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if allowed_methods:
|
||||||
|
self.headers = {
|
||||||
|
**self.headers,
|
||||||
|
"Allow": ", ".join(allowed_methods),
|
||||||
|
}
|
||||||
|
self.method = method
|
||||||
|
self.allowed_methods = allowed_methods
|
||||||
|
|
||||||
|
|
||||||
MethodNotSupported = MethodNotAllowed
|
MethodNotSupported = MethodNotAllowed
|
||||||
|
|
||||||
|
|
||||||
class ServerError(SanicException):
|
class ServerError(HTTPException):
|
||||||
"""
|
"""
|
||||||
**Status**: 500 Internal Server Error
|
**Status**: 500 Internal Server Error
|
||||||
|
|
||||||
|
A general server-side error has occurred. If no other HTTP exception is
|
||||||
|
appropriate, then this should be used
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Internal Server Error' will be sent,
|
||||||
|
defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 500
|
status_code = 500
|
||||||
|
|
||||||
|
|
||||||
class ServiceUnavailable(SanicException):
|
InternalServerError = ServerError
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUnavailable(HTTPException):
|
||||||
"""
|
"""
|
||||||
**Status**: 503 Service Unavailable
|
**Status**: 503 Service Unavailable
|
||||||
|
|
||||||
The server is currently unavailable (because it is overloaded or
|
The server is currently unavailable (because it is overloaded or
|
||||||
down for maintenance). Generally, this is a temporary state.
|
down for maintenance). Generally, this is a temporary state.
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Bad Request' will be sent, defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 503
|
status_code = 503
|
||||||
quiet = True
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class URLBuildError(ServerError):
|
class URLBuildError(HTTPException):
|
||||||
"""
|
"""
|
||||||
**Status**: 500 Internal Server Error
|
**Status**: 500 Internal Server Error
|
||||||
|
|
||||||
|
An exception used by Sanic internals when unable to build a URL.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 500
|
status_code = 500
|
||||||
@ -114,30 +304,77 @@ class URLBuildError(ServerError):
|
|||||||
class FileNotFound(NotFound):
|
class FileNotFound(NotFound):
|
||||||
"""
|
"""
|
||||||
**Status**: 404 Not Found
|
**Status**: 404 Not Found
|
||||||
|
|
||||||
|
A specific form of :class:`.NotFound` that is specifically when looking
|
||||||
|
for a file on the file system at a known path.
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Not Found' will be sent, defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param path: The path, if any, to the file that could not
|
||||||
|
be found, defaults to None
|
||||||
|
:type path: Optional[PathLike], optional
|
||||||
|
:param relative_url: A relative URL of the file, defaults to None
|
||||||
|
:type relative_url: Optional[str], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, message, path, relative_url):
|
def __init__(
|
||||||
super().__init__(message)
|
self,
|
||||||
|
message: Optional[Union[str, bytes]] = None,
|
||||||
|
path: Optional[PathLike] = None,
|
||||||
|
relative_url: Optional[str] = None,
|
||||||
|
*,
|
||||||
|
quiet: Optional[bool] = None,
|
||||||
|
context: Optional[Dict[str, Any]] = None,
|
||||||
|
extra: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
message,
|
||||||
|
quiet=quiet,
|
||||||
|
context=context,
|
||||||
|
extra=extra,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
self.path = path
|
self.path = path
|
||||||
self.relative_url = relative_url
|
self.relative_url = relative_url
|
||||||
|
|
||||||
|
|
||||||
class RequestTimeout(SanicException):
|
class RequestTimeout(HTTPException):
|
||||||
"""The Web server (running the Web site) thinks that there has been too
|
"""
|
||||||
|
The Web server (running the Web site) thinks that there has been too
|
||||||
long an interval of time between 1) the establishment of an IP
|
long an interval of time between 1) the establishment of an IP
|
||||||
connection (socket) between the client and the server and
|
connection (socket) between the client and the server and
|
||||||
2) the receipt of any data on that socket, so the server has dropped
|
2) the receipt of any data on that socket, so the server has dropped
|
||||||
the connection. The socket connection has actually been lost - the Web
|
the connection. The socket connection has actually been lost - the Web
|
||||||
server has 'timed out' on that particular socket connection.
|
server has 'timed out' on that particular socket connection.
|
||||||
|
|
||||||
|
This is an internal exception thrown by Sanic and should not be used
|
||||||
|
directly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 408
|
status_code = 408
|
||||||
quiet = True
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class PayloadTooLarge(SanicException):
|
class PayloadTooLarge(HTTPException):
|
||||||
"""
|
"""
|
||||||
**Status**: 413 Payload Too Large
|
**Status**: 413 Payload Too Large
|
||||||
|
|
||||||
|
This is an internal exception thrown by Sanic and should not be used
|
||||||
|
directly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 413
|
status_code = 413
|
||||||
@ -147,34 +384,126 @@ class PayloadTooLarge(SanicException):
|
|||||||
class HeaderNotFound(BadRequest):
|
class HeaderNotFound(BadRequest):
|
||||||
"""
|
"""
|
||||||
**Status**: 400 Bad Request
|
**Status**: 400 Bad Request
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Bad Request' will be sent, defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class InvalidHeader(BadRequest):
|
class InvalidHeader(BadRequest):
|
||||||
"""
|
"""
|
||||||
**Status**: 400 Bad Request
|
**Status**: 400 Bad Request
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Bad Request' will be sent, defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class RangeNotSatisfiable(SanicException):
|
class ContentRange(Protocol):
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
|
class RangeNotSatisfiable(HTTPException):
|
||||||
"""
|
"""
|
||||||
**Status**: 416 Range Not Satisfiable
|
**Status**: 416 Range Not Satisfiable
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Range Not Satisfiable' will be sent,
|
||||||
|
defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param content_range: An object meeting the :class:`.ContentRange` protocol
|
||||||
|
that has a ``total`` property, defaults to None
|
||||||
|
:type content_range: Optional[ContentRange], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 416
|
status_code = 416
|
||||||
quiet = True
|
quiet = True
|
||||||
|
|
||||||
def __init__(self, message, content_range):
|
def __init__(
|
||||||
super().__init__(message)
|
self,
|
||||||
self.headers = {"Content-Range": f"bytes */{content_range.total}"}
|
message: Optional[Union[str, bytes]] = None,
|
||||||
|
content_range: Optional[ContentRange] = None,
|
||||||
|
*,
|
||||||
|
quiet: Optional[bool] = None,
|
||||||
|
context: Optional[Dict[str, Any]] = None,
|
||||||
|
extra: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
message,
|
||||||
|
quiet=quiet,
|
||||||
|
context=context,
|
||||||
|
extra=extra,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if content_range is not None:
|
||||||
|
self.headers = {
|
||||||
|
**self.headers,
|
||||||
|
"Content-Range": f"bytes */{content_range.total}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ContentRangeError = RangeNotSatisfiable
|
ContentRangeError = RangeNotSatisfiable
|
||||||
|
|
||||||
|
|
||||||
class ExpectationFailed(SanicException):
|
class ExpectationFailed(HTTPException):
|
||||||
"""
|
"""
|
||||||
**Status**: 417 Expectation Failed
|
**Status**: 417 Expectation Failed
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Expectation Failed' will be sent,
|
||||||
|
defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 417
|
status_code = 417
|
||||||
@ -184,9 +513,25 @@ class ExpectationFailed(SanicException):
|
|||||||
HeaderExpectationFailed = ExpectationFailed
|
HeaderExpectationFailed = ExpectationFailed
|
||||||
|
|
||||||
|
|
||||||
class Forbidden(SanicException):
|
class Forbidden(HTTPException):
|
||||||
"""
|
"""
|
||||||
**Status**: 403 Forbidden
|
**Status**: 403 Forbidden
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Forbidden' will be sent, defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 403
|
status_code = 403
|
||||||
@ -202,20 +547,33 @@ class InvalidRangeType(RangeNotSatisfiable):
|
|||||||
quiet = True
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class PyFileError(Exception):
|
class PyFileError(SanicException):
|
||||||
def __init__(self, file):
|
def __init__(
|
||||||
super().__init__("could not execute config file %s", file)
|
self,
|
||||||
|
file,
|
||||||
|
status_code: Optional[int] = None,
|
||||||
|
*,
|
||||||
|
quiet: Optional[bool] = None,
|
||||||
|
context: Optional[Dict[str, Any]] = None,
|
||||||
|
extra: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, Any]] = None,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
"could not execute config file %s" % file,
|
||||||
|
status_code=status_code,
|
||||||
|
quiet=quiet,
|
||||||
|
context=context,
|
||||||
|
extra=extra,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Unauthorized(SanicException):
|
class Unauthorized(HTTPException):
|
||||||
"""
|
"""
|
||||||
**Status**: 401 Unauthorized
|
**Status**: 401 Unauthorized
|
||||||
|
|
||||||
:param message: Message describing the exception.
|
When present, additional keyword arguments may be used to complete
|
||||||
:param status_code: HTTP Status code.
|
the WWW-Authentication header.
|
||||||
:param scheme: Name of the authentication scheme to be used.
|
|
||||||
|
|
||||||
When present, kwargs is used to complete the WWW-Authentication header.
|
|
||||||
|
|
||||||
Examples::
|
Examples::
|
||||||
|
|
||||||
@ -240,21 +598,58 @@ class Unauthorized(SanicException):
|
|||||||
raise Unauthorized("Auth required.",
|
raise Unauthorized("Auth required.",
|
||||||
scheme="Bearer",
|
scheme="Bearer",
|
||||||
realm="Restricted Area")
|
realm="Restricted Area")
|
||||||
|
|
||||||
|
:param message: The message to be sent to the client. If ``None``
|
||||||
|
then the HTTP status 'Bad Request' will be sent, defaults to None
|
||||||
|
:type message: Optional[Union[str, bytes]], optional
|
||||||
|
:param scheme: Name of the authentication scheme to be used.
|
||||||
|
:type scheme: Optional[str], optional
|
||||||
|
:param quiet: When ``True``, the error traceback will be suppressed
|
||||||
|
from the logs, defaults to None
|
||||||
|
:type quiet: Optional[bool], optional
|
||||||
|
:param context: Additional mapping of key/value data that will be
|
||||||
|
sent to the client upon exception, defaults to None
|
||||||
|
:type context: Optional[Dict[str, Any]], optional
|
||||||
|
:param extra: Additional mapping of key/value data that will NOT be
|
||||||
|
sent to the client when in PRODUCTION mode, defaults to None
|
||||||
|
:type extra: Optional[Dict[str, Any]], optional
|
||||||
|
:param headers: Additional headers that should be sent with the HTTP
|
||||||
|
response, defaults to None
|
||||||
|
:type headers: Optional[Dict[str, Any]], optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 401
|
status_code = 401
|
||||||
quiet = True
|
quiet = True
|
||||||
|
|
||||||
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
def __init__(
|
||||||
super().__init__(message, status_code)
|
self,
|
||||||
|
message: Optional[Union[str, bytes]] = None,
|
||||||
|
scheme: Optional[str] = None,
|
||||||
|
*,
|
||||||
|
quiet: Optional[bool] = None,
|
||||||
|
context: Optional[Dict[str, Any]] = None,
|
||||||
|
extra: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, Any]] = None,
|
||||||
|
**challenges,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
message,
|
||||||
|
quiet=quiet,
|
||||||
|
context=context,
|
||||||
|
extra=extra,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
# if auth-scheme is specified, set "WWW-Authenticate" header
|
# if auth-scheme is specified, set "WWW-Authenticate" header
|
||||||
if scheme is not None:
|
if scheme is not None:
|
||||||
values = ['{!s}="{!s}"'.format(k, v) for k, v in kwargs.items()]
|
values = [
|
||||||
|
'{!s}="{!s}"'.format(k, v) for k, v in challenges.items()
|
||||||
|
]
|
||||||
challenge = ", ".join(values)
|
challenge = ", ".join(values)
|
||||||
|
|
||||||
self.headers = {
|
self.headers = {
|
||||||
"WWW-Authenticate": f"{scheme} {challenge}".rstrip()
|
**self.headers,
|
||||||
|
"WWW-Authenticate": f"{scheme} {challenge}".rstrip(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,4 +75,4 @@ class ContentRangeHandler:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return self.size > 0
|
return hasattr(self, "size") and self.size > 0
|
||||||
|
@ -428,7 +428,9 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||||||
if self.request is None:
|
if self.request is None:
|
||||||
self.create_empty_request()
|
self.create_empty_request()
|
||||||
|
|
||||||
request_middleware = not isinstance(exception, ServiceUnavailable)
|
request_middleware = not isinstance(
|
||||||
|
exception, (ServiceUnavailable, RequestCancelled)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
await app.handle_exception(
|
await app.handle_exception(
|
||||||
self.request, exception, request_middleware
|
self.request, exception, request_middleware
|
||||||
|
@ -159,7 +159,7 @@ class CertSimple(SanicSSLContext):
|
|||||||
# try common aliases, rename to cert/key
|
# try common aliases, rename to cert/key
|
||||||
certfile = kw["cert"] = kw.pop("certificate", None) or cert
|
certfile = kw["cert"] = kw.pop("certificate", None) or cert
|
||||||
keyfile = kw["key"] = kw.pop("keyfile", None) or key
|
keyfile = kw["key"] = kw.pop("keyfile", None) or key
|
||||||
password = kw.pop("password", None)
|
password = kw.get("password", None)
|
||||||
if not certfile or not keyfile:
|
if not certfile or not keyfile:
|
||||||
raise ValueError("SSL dict needs filenames for cert and key.")
|
raise ValueError("SSL dict needs filenames for cert and key.")
|
||||||
subject = {}
|
subject = {}
|
||||||
|
@ -62,13 +62,13 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
|||||||
},
|
},
|
||||||
formatters={
|
formatters={
|
||||||
"generic": {
|
"generic": {
|
||||||
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
|
"format": "%(asctime)s [%(process)s] [%(levelname)s] %(message)s",
|
||||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||||
"class": "logging.Formatter",
|
"class": "logging.Formatter",
|
||||||
},
|
},
|
||||||
"access": {
|
"access": {
|
||||||
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
|
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: "
|
||||||
+ "%(request)s %(message)s %(status)d %(byte)d",
|
+ "%(request)s %(message)s %(status)s %(byte)s",
|
||||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||||
"class": "logging.Formatter",
|
"class": "logging.Formatter",
|
||||||
},
|
},
|
||||||
|
@ -811,7 +811,7 @@ class StartupMixin(metaclass=SanicMeta):
|
|||||||
ssl = kwargs.get("ssl")
|
ssl = kwargs.get("ssl")
|
||||||
|
|
||||||
if isinstance(ssl, SanicSSLContext):
|
if isinstance(ssl, SanicSSLContext):
|
||||||
kwargs["ssl"] = kwargs["ssl"].sanic
|
kwargs["ssl"] = ssl.sanic
|
||||||
|
|
||||||
manager = WorkerManager(
|
manager = WorkerManager(
|
||||||
primary.state.workers,
|
primary.state.workers,
|
||||||
|
@ -148,7 +148,26 @@ async def validate_file(
|
|||||||
last_modified = datetime.fromtimestamp(
|
last_modified = datetime.fromtimestamp(
|
||||||
float(last_modified), tz=timezone.utc
|
float(last_modified), tz=timezone.utc
|
||||||
).replace(microsecond=0)
|
).replace(microsecond=0)
|
||||||
if last_modified <= if_modified_since:
|
|
||||||
|
if (
|
||||||
|
last_modified.utcoffset() is None
|
||||||
|
and if_modified_since.utcoffset() is not None
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
"Cannot compare tz-aware and tz-naive datetimes. To avoid "
|
||||||
|
"this conflict Sanic is converting last_modified to UTC."
|
||||||
|
)
|
||||||
|
last_modified.replace(tzinfo=timezone.utc)
|
||||||
|
elif (
|
||||||
|
last_modified.utcoffset() is not None
|
||||||
|
and if_modified_since.utcoffset() is None
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
"Cannot compare tz-aware and tz-naive datetimes. To avoid "
|
||||||
|
"this conflict Sanic is converting if_modified_since to UTC."
|
||||||
|
)
|
||||||
|
if_modified_since.replace(tzinfo=timezone.utc)
|
||||||
|
if last_modified.timestamp() <= if_modified_since.timestamp():
|
||||||
return HTTPResponse(status=304)
|
return HTTPResponse(status=304)
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ except ImportError: # websockets >= 11.0
|
|||||||
|
|
||||||
from websockets.typing import Subprotocol
|
from websockets.typing import Subprotocol
|
||||||
|
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import SanicException
|
||||||
from sanic.log import logger
|
from sanic.log import logger
|
||||||
from sanic.server import HttpProtocol
|
from sanic.server import HttpProtocol
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
"Failed to open a WebSocket connection.\n"
|
"Failed to open a WebSocket connection.\n"
|
||||||
"See server log for more information.\n"
|
"See server log for more information.\n"
|
||||||
)
|
)
|
||||||
raise ServerError(msg, status_code=500)
|
raise SanicException(msg, status_code=500)
|
||||||
if 100 <= resp.status_code <= 299:
|
if 100 <= resp.status_code <= 299:
|
||||||
first_line = (
|
first_line = (
|
||||||
f"HTTP/1.1 {resp.status_code} {resp.reason_phrase}\r\n"
|
f"HTTP/1.1 {resp.status_code} {resp.reason_phrase}\r\n"
|
||||||
@ -138,7 +138,7 @@ class WebSocketProtocol(HttpProtocol):
|
|||||||
rbody += b"\r\n\r\n"
|
rbody += b"\r\n\r\n"
|
||||||
await super().send(rbody)
|
await super().send(rbody)
|
||||||
else:
|
else:
|
||||||
raise ServerError(resp.body, resp.status_code)
|
raise SanicException(resp.body, resp.status_code)
|
||||||
self.websocket = WebsocketImplProtocol(
|
self.websocket = WebsocketImplProtocol(
|
||||||
ws_proto,
|
ws_proto,
|
||||||
ping_interval=self.websocket_ping_interval,
|
ping_interval=self.websocket_ping_interval,
|
||||||
|
@ -5,6 +5,7 @@ import sys
|
|||||||
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from ssl import SSLContext
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast
|
||||||
|
|
||||||
from sanic.http.tls.context import process_to_context
|
from sanic.http.tls.context import process_to_context
|
||||||
@ -103,8 +104,16 @@ class CertLoader:
|
|||||||
"trustme": TrustmeCreator,
|
"trustme": TrustmeCreator,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, ssl_data: Dict[str, Union[str, os.PathLike]]):
|
def __init__(
|
||||||
|
self,
|
||||||
|
ssl_data: Optional[
|
||||||
|
Union[SSLContext, Dict[str, Union[str, os.PathLike]]]
|
||||||
|
],
|
||||||
|
):
|
||||||
self._ssl_data = ssl_data
|
self._ssl_data = ssl_data
|
||||||
|
self._creator_class = None
|
||||||
|
if not ssl_data or not isinstance(ssl_data, dict):
|
||||||
|
return
|
||||||
|
|
||||||
creator_name = cast(str, ssl_data.get("creator"))
|
creator_name = cast(str, ssl_data.get("creator"))
|
||||||
|
|
||||||
|
@ -73,8 +73,8 @@ def worker_serve(
|
|||||||
info.settings["app"] = a
|
info.settings["app"] = a
|
||||||
a.state.server_info.append(info)
|
a.state.server_info.append(info)
|
||||||
|
|
||||||
if isinstance(ssl, dict):
|
if isinstance(ssl, dict) or app.certloader_class is not CertLoader:
|
||||||
cert_loader = CertLoader(ssl)
|
cert_loader = app.certloader_class(ssl or {})
|
||||||
ssl = cert_loader.load(app)
|
ssl = cert_loader.load(app)
|
||||||
for info in app.state.server_info:
|
for info in app.state.server_info:
|
||||||
info.settings["ssl"] = ssl
|
info.settings["ssl"] = ssl
|
||||||
|
19
tests/certs/password/fullchain.pem
Normal file
19
tests/certs/password/fullchain.pem
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDCTCCAfGgAwIBAgIUa7OOlAGQfXOgUgRENJ9GbUgO7kwwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFDESMBAGA1UEAwwJMTI3LjAuMC4xMB4XDTIzMDMyMDA3MzE1M1oXDTIzMDQx
|
||||||
|
OTA3MzE1M1owFDESMBAGA1UEAwwJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEF
|
||||||
|
AAOCAQ8AMIIBCgKCAQEAn2/RqVpzO7GFrgVGiowR5CzcFzf1tSFti1K/WIGr/jsu
|
||||||
|
NP+1R3sim17pgg6SCOFnUMRS0KnDihkzoeP6z+0tFsrbCH4V1+fq0iud8WgYQrgD
|
||||||
|
3ttUcHrz04p7wsMoeqndUQoLbyJzP8MpA2XJsoacdIVkuLv2AESGXLhJym/e9HGN
|
||||||
|
g8bqdz25X0hVTczZW1FN9AZyWWVf9Go6jqC7LCaOnYXAnOkEy2/JHdkeNXYFZHB3
|
||||||
|
71UemfkCjfp0vlRV8pVpkBGMhRNFphBTfxdqeWiGQwVqrhaJO4M7DJlQHCAPY16P
|
||||||
|
o9ywnhLDhFHD7KIfTih9XxrdgTowqcwyGX3e3aJpTwIDAQABo1MwUTAdBgNVHQ4E
|
||||||
|
FgQU5NogMq6mRBeGl4i6hIuUlcR2bVEwHwYDVR0jBBgwFoAU5NogMq6mRBeGl4i6
|
||||||
|
hIuUlcR2bVEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAYW34
|
||||||
|
JY1kd0UO5HE41oxJD4PioQboXXX0al4RgKaUUsPykeHQbK0q0TSYAZLwRjooTVUO
|
||||||
|
Wvna5bU2mzyULqA2r/Cr/w4zb9xybO3SiHFHcU1RacouauHXROHwRm98i8A73xnH
|
||||||
|
vHws5BADr2ggnVcPNh4VOQ9ZvBlC7jhgpvMjqOEu5ZPCovhfZYfSsvBDHcD74ZYm
|
||||||
|
Di9DvqsJmrb23Dv3SUykm3W+Ql2q+JyjFj30rhD89CFwJ9iSlFwTYEwZLHA+mV6p
|
||||||
|
UKy3I3Fiht1Oc+nIivX5uhRSMbDVvDTVHbjjPujxxFjkiHXMjtwvwfg4Sb6du61q
|
||||||
|
AjBRFyXbNu4hZkkHOA==
|
||||||
|
-----END CERTIFICATE-----
|
30
tests/certs/password/privkey.pem
Normal file
30
tests/certs/password/privkey.pem
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||||
|
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQI94UBqjaZlG4CAggA
|
||||||
|
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCvJhEy+3/+0Ec0gpd5dkP6BIIE
|
||||||
|
0E7rLplTe9rxK3sR9V0cx8Xn6V+uFhG3p7dzeMDCCKpGo9MEaacF5m+paGnBkMlH
|
||||||
|
Pz3rRoLA5jqzwXl4US/C5E1Or//2YBgF1XXKi3BPF/bVx/g6vR+xeobf9kQGbqQk
|
||||||
|
FNPYtP7mpg2dekp5BUsKSosIt8BkknWFvhBeNuGZT/zlMUuq1WpMe4KIh/W9IdNr
|
||||||
|
HolcuZJWBhQAwGPciWIZRyq48wKa++W7Jdg/aG8FviJQnjaAUv4CyZJHUJnaNwUx
|
||||||
|
iHOETpzIC+bhF2K+s4g5w68VCj6Jtz78sIBEZKzo7LI5QHdRHqYB5SJ/dGiV+h09
|
||||||
|
R/rQ/M+24mwHDlRSCxxq0yuDwUuGBlHyATeDCFeE3L5OX8yTLuqYJ6vUa6UbzMYA
|
||||||
|
8H4l5zfu9RrAhKYa9tD+4ONxMmHziIgmn5zvSXeBwJKfeUbnN4IKWLsSoSVspBRh
|
||||||
|
zLl51DMAnem4NEjLfIW8WYjhsvSYwd9BYqxXaAiv4Wjx9ZV1yLqFICC7tejpVdRT
|
||||||
|
afI0qMOfWu4ma6xVBg1ezLgF1wHIPrq6euTvWdnifYQopVICALlltEo5oxQ2i/OM
|
||||||
|
NY8RyovWujiGNsa3pId9HmZXiLyLXjKPstGWRK4liMyc2EiP099gTdBvrb+VQp+I
|
||||||
|
EyPavmh3WNhgZGOh3qah39X8HrBprc0PPfSPlxpaWdNMIIMSbcIWWdJEA/e4tcy/
|
||||||
|
uBaV4H3sNCtBApgrb6B9YUbS9CXNUburJo19T1sk2uCaO12qYfdu2IDEnFf8JiF3
|
||||||
|
i7nyftotRuoKq2D+V8d0PeMi/vJSo6+eZIn7VNe6ejYf+w0s7sxlpiKVzkslyOhq
|
||||||
|
n0T4M3ZkSwGIETzgkRRuTY1OK7slhglMgXlQ2FuIUUo6CRg9WjRJvI5rujLzLWfB
|
||||||
|
hkgP8STirjTV0DUWPFGtUcenvEcZPkYIQcoPHxOJGNW3ZPXNpt4RjbvPLeVzDm0O
|
||||||
|
WJiay/qhag/bXGqKraO3b6Y7FOzJa8kG4G0XrcFY1s2oCXRqRqYJAtwaEeVCjCSJ
|
||||||
|
Qy0OZkqcJEU7pv98pLMpG9OWz4Gle77g4KoQUJjQGtmg0MUMoPd0iPRmvkxsYg8E
|
||||||
|
Q9uZS3m6PpWmmYDY0Ik1w/4avs3skl2mW3dqcZGLEepkjiQSnFABsuvxKd+uIEQy
|
||||||
|
lyf9FrynXVcUI87LUkuniLRKwZZzFALVuc+BwtO3SA5mvEK22ZEq9QOysbwlpN54
|
||||||
|
G5xXJKJEeexUSjEUIij4J89RLsXldibhp7YYZ7rFviR6chIqC0V7G6VqAM9TOCrV
|
||||||
|
PWZXr3ZY5/pCZYs5DYKFJBFMSQ2UT/++VYxdZCeBH75vaxugbS8RdUM+iVDevWpQ
|
||||||
|
/AnP1FolNAgkVhi3Rw4L16SibkqpEzIi1svPWKMwXdvewA32UidLElhuTWWjI2Wm
|
||||||
|
veXhmEqwk/7ML4JMI7wHcDQdvSKen0mCL2J9tB7A/pewYyDE0ffIUmjxglOtw30f
|
||||||
|
ZOlQKhMaKJGXp00U2zsHA2NJRI/hThbJncsnZyvuLei0P42RrF+r64b/0gUH6IZ5
|
||||||
|
wPUttT815KSNoy+XXXum9YGDYYFoAL+6WVEkl6dgo+X0hcH7DDf5Nkewiq8UcJGh
|
||||||
|
/69vFIfp+JlpicXzZ+R42LO3T3luC907aFBywF3pmi//
|
||||||
|
-----END ENCRYPTED PRIVATE KEY-----
|
@ -7,6 +7,9 @@ from unittest.mock import call
|
|||||||
import pytest
|
import pytest
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
from httpx import Headers
|
||||||
|
from pytest import MonkeyPatch
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.application.state import Mode
|
from sanic.application.state import Mode
|
||||||
from sanic.asgi import ASGIApp, Lifespan, MockTransport
|
from sanic.asgi import ASGIApp, Lifespan, MockTransport
|
||||||
@ -626,3 +629,26 @@ async def test_error_on_lifespan_exception_stop(app: Sanic):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_asgi_headers_decoding(app: Sanic, monkeypatch: MonkeyPatch):
|
||||||
|
@app.get("/")
|
||||||
|
def handler(request: Request):
|
||||||
|
return text("")
|
||||||
|
|
||||||
|
headers_init = Headers.__init__
|
||||||
|
|
||||||
|
def mocked_headers_init(self, *args, **kwargs):
|
||||||
|
if "encoding" in kwargs:
|
||||||
|
kwargs.pop("encoding")
|
||||||
|
headers_init(self, encoding="utf-8", *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(Headers, "__init__", mocked_headers_init)
|
||||||
|
|
||||||
|
message = "Header names can only contain US-ASCII characters"
|
||||||
|
with pytest.raises(BadRequest, match=message):
|
||||||
|
_, response = await app.asgi_client.get("/", headers={"😂": "😅"})
|
||||||
|
|
||||||
|
_, response = await app.asgi_client.get("/", headers={"Test-Header": "😅"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
54
tests/test_late_adds.py
Normal file
54
tests/test_late_adds.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from sanic import Sanic, text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def late_app(app: Sanic):
|
||||||
|
app.config.TOUCHUP = False
|
||||||
|
app.get("/")(lambda _: text(""))
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def test_late_route(late_app: Sanic):
|
||||||
|
@late_app.before_server_start
|
||||||
|
async def late(app: Sanic):
|
||||||
|
@app.get("/late")
|
||||||
|
def handler(_):
|
||||||
|
return text("late")
|
||||||
|
|
||||||
|
_, response = late_app.test_client.get("/late")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.text == "late"
|
||||||
|
|
||||||
|
|
||||||
|
def test_late_middleware(late_app: Sanic):
|
||||||
|
@late_app.get("/late")
|
||||||
|
def handler(request):
|
||||||
|
return text(request.ctx.late)
|
||||||
|
|
||||||
|
@late_app.before_server_start
|
||||||
|
async def late(app: Sanic):
|
||||||
|
@app.on_request
|
||||||
|
def handler(request):
|
||||||
|
request.ctx.late = "late"
|
||||||
|
|
||||||
|
_, response = late_app.test_client.get("/late")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.text == "late"
|
||||||
|
|
||||||
|
|
||||||
|
def test_late_signal(late_app: Sanic):
|
||||||
|
@late_app.get("/late")
|
||||||
|
def handler(request):
|
||||||
|
return text(request.ctx.late)
|
||||||
|
|
||||||
|
@late_app.before_server_start
|
||||||
|
async def late(app: Sanic):
|
||||||
|
@app.signal("http.lifecycle.request")
|
||||||
|
def handler(request):
|
||||||
|
request.ctx.late = "late"
|
||||||
|
|
||||||
|
_, response = late_app.test_client.get("/late")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.text == "late"
|
55
tests/test_response_file.py
Normal file
55
tests/test_response_file.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from logging import INFO
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sanic.compat import Header
|
||||||
|
from sanic.response.convenience import validate_file
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"ifmod,lastmod,expected",
|
||||||
|
(
|
||||||
|
("Sat, 01 Apr 2023 00:00:00 GMT", 1672524000, None),
|
||||||
|
(
|
||||||
|
"Sat, 01 Apr 2023 00:00:00",
|
||||||
|
1672524000,
|
||||||
|
"converting if_modified_since",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Sat, 01 Apr 2023 00:00:00 GMT",
|
||||||
|
datetime(2023, 1, 1, 0, 0, 0),
|
||||||
|
"converting last_modified",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Sat, 01 Apr 2023 00:00:00",
|
||||||
|
datetime(2023, 1, 1, 0, 0, 0),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Sat, 01 Apr 2023 00:00:00 GMT",
|
||||||
|
datetime(2023, 1, 1, 0, 0, 0).replace(tzinfo=timezone.utc),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Sat, 01 Apr 2023 00:00:00",
|
||||||
|
datetime(2023, 1, 1, 0, 0, 0).replace(tzinfo=timezone.utc),
|
||||||
|
"converting if_modified_since",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_file_timestamp_validation(
|
||||||
|
lastmod, ifmod, expected, caplog: pytest.LogCaptureFixture
|
||||||
|
):
|
||||||
|
headers = Header([["If-Modified-Since", ifmod]])
|
||||||
|
|
||||||
|
with caplog.at_level(INFO):
|
||||||
|
response = await validate_file(headers, lastmod)
|
||||||
|
assert response.status == 304
|
||||||
|
records = caplog.records
|
||||||
|
if not expected:
|
||||||
|
assert len(records) == 0
|
||||||
|
else:
|
||||||
|
record = records[0]
|
||||||
|
assert expected in record.message
|
@ -12,7 +12,7 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic_testing.testing import HOST, PORT
|
from sanic_testing.testing import HOST, PORT, SanicTestClient
|
||||||
|
|
||||||
import sanic.http.tls.creators
|
import sanic.http.tls.creators
|
||||||
|
|
||||||
@ -29,16 +29,24 @@ from sanic.http.tls.creators import (
|
|||||||
get_ssl_context,
|
get_ssl_context,
|
||||||
)
|
)
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
from sanic.worker.loader import CertLoader
|
||||||
|
|
||||||
|
|
||||||
current_dir = os.path.dirname(os.path.realpath(__file__))
|
current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
localhost_dir = os.path.join(current_dir, "certs/localhost")
|
localhost_dir = os.path.join(current_dir, "certs/localhost")
|
||||||
|
password_dir = os.path.join(current_dir, "certs/password")
|
||||||
sanic_dir = os.path.join(current_dir, "certs/sanic.example")
|
sanic_dir = os.path.join(current_dir, "certs/sanic.example")
|
||||||
invalid_dir = os.path.join(current_dir, "certs/invalid.nonexist")
|
invalid_dir = os.path.join(current_dir, "certs/invalid.nonexist")
|
||||||
localhost_cert = os.path.join(localhost_dir, "fullchain.pem")
|
localhost_cert = os.path.join(localhost_dir, "fullchain.pem")
|
||||||
localhost_key = os.path.join(localhost_dir, "privkey.pem")
|
localhost_key = os.path.join(localhost_dir, "privkey.pem")
|
||||||
sanic_cert = os.path.join(sanic_dir, "fullchain.pem")
|
sanic_cert = os.path.join(sanic_dir, "fullchain.pem")
|
||||||
sanic_key = os.path.join(sanic_dir, "privkey.pem")
|
sanic_key = os.path.join(sanic_dir, "privkey.pem")
|
||||||
|
password_dict = {
|
||||||
|
"cert": os.path.join(password_dir, "fullchain.pem"),
|
||||||
|
"key": os.path.join(password_dir, "privkey.pem"),
|
||||||
|
"password": "password",
|
||||||
|
"names": ["localhost"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -420,6 +428,29 @@ def test_no_certs_on_list(app):
|
|||||||
assert "No certificates" in str(excinfo.value)
|
assert "No certificates" in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_cert_loader():
|
||||||
|
class MyCertLoader(CertLoader):
|
||||||
|
def load(self, app: Sanic):
|
||||||
|
self._ssl_data = {
|
||||||
|
"key": localhost_key,
|
||||||
|
"cert": localhost_cert,
|
||||||
|
}
|
||||||
|
return super().load(app)
|
||||||
|
|
||||||
|
app = Sanic("custom", certloader_class=MyCertLoader)
|
||||||
|
|
||||||
|
@app.get("/test")
|
||||||
|
async def handler(request):
|
||||||
|
return text("ssl test")
|
||||||
|
|
||||||
|
client = SanicTestClient(app, port=44556)
|
||||||
|
|
||||||
|
request, response = client.get("https://localhost:44556/test")
|
||||||
|
assert request.scheme == "https"
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.text == "ssl test"
|
||||||
|
|
||||||
|
|
||||||
def test_logger_vhosts(caplog):
|
def test_logger_vhosts(caplog):
|
||||||
app = Sanic(name="test_logger_vhosts")
|
app = Sanic(name="test_logger_vhosts")
|
||||||
|
|
||||||
@ -677,3 +708,34 @@ def test_ssl_in_multiprocess_mode(app: Sanic, caplog):
|
|||||||
logging.INFO,
|
logging.INFO,
|
||||||
"Goin' Fast @ https://127.0.0.1:8000",
|
"Goin' Fast @ https://127.0.0.1:8000",
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
sys.platform not in ("linux", "darwin"),
|
||||||
|
reason="This test requires fork context",
|
||||||
|
)
|
||||||
|
def test_ssl_in_multiprocess_mode_password(
|
||||||
|
app: Sanic, caplog: pytest.LogCaptureFixture
|
||||||
|
):
|
||||||
|
event = Event()
|
||||||
|
|
||||||
|
@app.main_process_start
|
||||||
|
async def main_start(app: Sanic):
|
||||||
|
app.shared_ctx.event = event
|
||||||
|
|
||||||
|
@app.after_server_start
|
||||||
|
async def shutdown(app):
|
||||||
|
app.shared_ctx.event.set()
|
||||||
|
app.stop()
|
||||||
|
|
||||||
|
assert not event.is_set()
|
||||||
|
with use_context("fork"):
|
||||||
|
with caplog.at_level(logging.INFO):
|
||||||
|
app.run(ssl=password_dict)
|
||||||
|
assert event.is_set()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
"sanic.root",
|
||||||
|
logging.INFO,
|
||||||
|
"Goin' Fast @ https://127.0.0.1:8000",
|
||||||
|
) in caplog.record_tuples
|
||||||
|
Loading…
x
Reference in New Issue
Block a user