Router tweaks (#2031)
* Add trailing slash when defined and strict_slashes * Add partial matching, and fix some issues with url_for * Cover additional edge cases * cleanup tests
This commit is contained in:
36
sanic/app.py
36
sanic/app.py
@@ -393,17 +393,22 @@ class Sanic(BaseSanic):
|
||||
if getattr(route.ctx, "static", None):
|
||||
filename = kwargs.pop("filename", "")
|
||||
# it's static folder
|
||||
if "file_uri" in uri:
|
||||
folder_ = uri.split("<file_uri:", 1)[0]
|
||||
if "__file_uri__" in uri:
|
||||
folder_ = uri.split("<__file_uri__:", 1)[0]
|
||||
if folder_.endswith("/"):
|
||||
folder_ = folder_[:-1]
|
||||
|
||||
if filename.startswith("/"):
|
||||
filename = filename[1:]
|
||||
|
||||
kwargs["file_uri"] = filename
|
||||
kwargs["__file_uri__"] = filename
|
||||
|
||||
if uri != "/" and uri.endswith("/"):
|
||||
if (
|
||||
uri != "/"
|
||||
and uri.endswith("/")
|
||||
and not route.strict
|
||||
and not route.raw_path[:-1]
|
||||
):
|
||||
uri = uri[:-1]
|
||||
|
||||
if not uri.startswith("/"):
|
||||
@@ -573,25 +578,27 @@ class Sanic(BaseSanic):
|
||||
# Define `response` var here to remove warnings about
|
||||
# allocation before assignment below.
|
||||
response = None
|
||||
name = None
|
||||
try:
|
||||
# Fetch handler from router
|
||||
(
|
||||
route,
|
||||
handler,
|
||||
kwargs,
|
||||
uri,
|
||||
name,
|
||||
ignore_body,
|
||||
) = self.router.get(request)
|
||||
request.name = name
|
||||
|
||||
request._match_info = kwargs
|
||||
request.route = route
|
||||
request.name = route.name
|
||||
request.uri_template = f"/{route.path}"
|
||||
request.endpoint = request.name
|
||||
|
||||
if (
|
||||
request.stream
|
||||
and request.stream.request_body
|
||||
and not ignore_body
|
||||
and not route.ctx.ignore_body
|
||||
):
|
||||
if self.router.is_stream_handler(request):
|
||||
|
||||
if hasattr(handler, "is_stream"):
|
||||
# Streaming handler: lift the size limit
|
||||
request.stream.request_max_size = float("inf")
|
||||
else:
|
||||
@@ -602,15 +609,15 @@ class Sanic(BaseSanic):
|
||||
# Request Middleware
|
||||
# -------------------------------------------- #
|
||||
response = await self._run_request_middleware(
|
||||
request, request_name=name
|
||||
request, request_name=route.name
|
||||
)
|
||||
|
||||
# No middleware results
|
||||
if not response:
|
||||
# -------------------------------------------- #
|
||||
# Execute Handler
|
||||
# -------------------------------------------- #
|
||||
|
||||
request.uri_template = f"/{uri}"
|
||||
if handler is None:
|
||||
raise ServerError(
|
||||
(
|
||||
@@ -619,12 +626,11 @@ class Sanic(BaseSanic):
|
||||
)
|
||||
)
|
||||
|
||||
request.endpoint = request.name
|
||||
|
||||
# Run response handler
|
||||
response = handler(request, **kwargs)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
|
||||
if response:
|
||||
response = await request.respond(response)
|
||||
else:
|
||||
|
||||
@@ -611,7 +611,7 @@ class RouteMixin:
|
||||
else:
|
||||
break
|
||||
|
||||
if not name: # noq
|
||||
if not name: # noqa
|
||||
raise ValueError("Could not generate a name for handler")
|
||||
|
||||
if not name.startswith(f"{self.name}."):
|
||||
@@ -627,19 +627,19 @@ class RouteMixin:
|
||||
stream_large_files,
|
||||
request,
|
||||
content_type=None,
|
||||
file_uri=None,
|
||||
__file_uri__=None,
|
||||
):
|
||||
# Using this to determine if the URL is trying to break out of the path
|
||||
# served. os.path.realpath seems to be very slow
|
||||
if file_uri and "../" in file_uri:
|
||||
if __file_uri__ and "../" in __file_uri__:
|
||||
raise InvalidUsage("Invalid URL")
|
||||
# Merge served directory and requested file if provided
|
||||
# Strip all / that in the beginning of the URL to help prevent python
|
||||
# from herping a derp and treating the uri as an absolute path
|
||||
root_path = file_path = file_or_directory
|
||||
if file_uri:
|
||||
if __file_uri__:
|
||||
file_path = path.join(
|
||||
file_or_directory, sub("^[/]*", "", file_uri)
|
||||
file_or_directory, sub("^[/]*", "", __file_uri__)
|
||||
)
|
||||
|
||||
# URL decode the path sent by the browser otherwise we won't be able to
|
||||
@@ -648,10 +648,12 @@ class RouteMixin:
|
||||
if not file_path.startswith(path.abspath(unquote(root_path))):
|
||||
error_logger.exception(
|
||||
f"File not found: path={file_or_directory}, "
|
||||
f"relative_url={file_uri}"
|
||||
f"relative_url={__file_uri__}"
|
||||
)
|
||||
raise FileNotFound(
|
||||
"File not found", path=file_or_directory, relative_url=file_uri
|
||||
"File not found",
|
||||
path=file_or_directory,
|
||||
relative_url=__file_uri__,
|
||||
)
|
||||
try:
|
||||
headers = {}
|
||||
@@ -719,10 +721,12 @@ class RouteMixin:
|
||||
except Exception:
|
||||
error_logger.exception(
|
||||
f"File not found: path={file_or_directory}, "
|
||||
f"relative_url={file_uri}"
|
||||
f"relative_url={__file_uri__}"
|
||||
)
|
||||
raise FileNotFound(
|
||||
"File not found", path=file_or_directory, relative_url=file_uri
|
||||
"File not found",
|
||||
path=file_or_directory,
|
||||
relative_url=__file_uri__,
|
||||
)
|
||||
|
||||
def _register_static(
|
||||
@@ -772,7 +776,7 @@ class RouteMixin:
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
if not path.isfile(file_or_directory):
|
||||
uri += "/<file_uri>"
|
||||
uri += "/<__file_uri__>"
|
||||
|
||||
# special prefix for static files
|
||||
# if not static.name.startswith("_static_"):
|
||||
|
||||
@@ -12,6 +12,8 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.server import ConnInfo
|
||||
@@ -104,6 +106,7 @@ class Request:
|
||||
"parsed_forwarded",
|
||||
"raw_url",
|
||||
"request_middleware_started",
|
||||
"route",
|
||||
"stream",
|
||||
"transport",
|
||||
"uri_template",
|
||||
@@ -151,6 +154,7 @@ class Request:
|
||||
self._match_info: Dict[str, Any] = {}
|
||||
self.stream: Optional[Http] = None
|
||||
self.endpoint: Optional[str] = None
|
||||
self.route: Optional[Route] = None
|
||||
|
||||
def __repr__(self):
|
||||
class_name = self.__class__.__name__
|
||||
@@ -431,6 +435,7 @@ class Request:
|
||||
:return: Incoming cookies on the request
|
||||
:rtype: Dict[str, str]
|
||||
"""
|
||||
|
||||
if self._cookies is None:
|
||||
cookie = self.headers.get("Cookie")
|
||||
if cookie is not None:
|
||||
|
||||
@@ -9,12 +9,13 @@ from sanic_routing.exceptions import (
|
||||
from sanic_routing.route import Route # type: ignore
|
||||
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import MethodNotSupported, NotFound
|
||||
from sanic.exceptions import MethodNotSupported, NotFound, SanicException
|
||||
from sanic.handlers import RouteHandler
|
||||
from sanic.request import Request
|
||||
|
||||
|
||||
ROUTER_CACHE_SIZE = 1024
|
||||
ALLOWED_LABELS = ("__file_uri__",)
|
||||
|
||||
|
||||
class Router(BaseRouter):
|
||||
@@ -33,7 +34,7 @@ class Router(BaseRouter):
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def _get(
|
||||
self, path, method, host
|
||||
) -> Tuple[RouteHandler, Dict[str, Any], str, str, bool]:
|
||||
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
|
||||
try:
|
||||
route, handler, params = self.resolve(
|
||||
path=path,
|
||||
@@ -50,14 +51,14 @@ class Router(BaseRouter):
|
||||
)
|
||||
|
||||
return (
|
||||
route,
|
||||
handler,
|
||||
params,
|
||||
route.path,
|
||||
route.name,
|
||||
route.ctx.ignore_body,
|
||||
)
|
||||
|
||||
def get(self, request: Request):
|
||||
def get( # type: ignore
|
||||
self, request: Request
|
||||
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve a `Route` object containg the details about how to handle
|
||||
a response for a given request
|
||||
@@ -66,14 +67,13 @@ class Router(BaseRouter):
|
||||
:type request: Request
|
||||
:return: details needed for handling the request and returning the
|
||||
correct response
|
||||
:rtype: Tuple[ RouteHandler, Tuple[Any, ...], Dict[str, Any], str, str,
|
||||
Optional[str], bool, ]
|
||||
:rtype: Tuple[ Route, RouteHandler, Dict[str, Any]]
|
||||
"""
|
||||
return self._get(
|
||||
request.path, request.method, request.headers.get("host")
|
||||
)
|
||||
|
||||
def add(
|
||||
def add( # type: ignore
|
||||
self,
|
||||
uri: str,
|
||||
methods: Iterable[str],
|
||||
@@ -138,7 +138,7 @@ class Router(BaseRouter):
|
||||
if host:
|
||||
params.update({"requirements": {"host": host}})
|
||||
|
||||
route = super().add(**params)
|
||||
route = super().add(**params) # type: ignore
|
||||
route.ctx.ignore_body = ignore_body
|
||||
route.ctx.stream = stream
|
||||
route.ctx.hosts = hosts
|
||||
@@ -150,23 +150,6 @@ class Router(BaseRouter):
|
||||
return routes[0]
|
||||
return routes
|
||||
|
||||
def is_stream_handler(self, request) -> bool:
|
||||
"""
|
||||
Handler for request is stream or not.
|
||||
|
||||
:param request: Request object
|
||||
:return: bool
|
||||
"""
|
||||
try:
|
||||
handler = self.get(request)[0]
|
||||
except (NotFound, MethodNotSupported):
|
||||
return False
|
||||
if hasattr(handler, "view_class") and hasattr(
|
||||
handler.view_class, request.method.lower()
|
||||
):
|
||||
handler = getattr(handler.view_class, request.method.lower())
|
||||
return hasattr(handler, "is_stream")
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def find_route_by_view_name(self, view_name, name=None):
|
||||
"""
|
||||
@@ -204,3 +187,15 @@ class Router(BaseRouter):
|
||||
@property
|
||||
def routes_regex(self):
|
||||
return self.regex_routes
|
||||
|
||||
def finalize(self, *args, **kwargs):
|
||||
super().finalize(*args, **kwargs)
|
||||
|
||||
for route in self.dynamic_routes.values():
|
||||
if any(
|
||||
label.startswith("__") and label not in ALLOWED_LABELS
|
||||
for label in route.labels
|
||||
):
|
||||
raise SanicException(
|
||||
f"Invalid route: {route}. Parameter names cannot use '__'."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user