diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 28825a0c..98b06783 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -5,21 +5,22 @@ about: Create a report to help us improve --- **Describe the bug** -A clear and concise description of what the bug is, make sure to paste any exceptions and tracebacks. + **Code snippet** -Relevant source code, make sure to remove what is not necessary. + **Expected behavior** -A clear and concise description of what you expected to happen. + **Environment (please complete the following information):** - - OS: [e.g. iOS] - - Version [e.g. 0.8.3] + + - OS: + - Sanic Version: **Additional context** -Add any other context about the problem here. + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index bcd3f219..61c8ec40 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,3 +3,6 @@ contact_links: - name: Questions and Help url: https://community.sanicframework.org/c/questions-and-help about: Do you need help with Sanic? Ask your questions here. + - name: Discussion and Support + url: https://discord.gg/FARQzAEMAA + about: For live discussion and support, checkout the Sanic Discord server. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index e2384325..8227333c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -5,12 +5,12 @@ about: Suggest an idea for Sanic --- **Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + **Describe the solution you'd like** -A clear and concise description of what you want to happen. + **Additional context** -Add any other context or sample code about the feature request here. + diff --git a/SECURITY.md b/SECURITY.md index d49fce6f..71c03511 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,31 +4,40 @@ Sanic releases long term support release once a year in December. LTS releases receive bug and security updates for **24 months**. Interim releases throughout the year occur every three months, and are supported until the subsequent interim release. -| Version | LTS | Supported | -| ------- | ------------- | ------------------ | -| 20.12 | until 2022-12 | :heavy_check_mark: | -| 20.9 | | :x: | -| 20.6 | | :x: | -| 20.3 | | :x: | -| 19.12 | until 2021-12 | :white_check_mark: | -| 19.9 | | :x: | -| 19.6 | | :x: | -| 19.3 | | :x: | -| 18.12 | | :x: | -| 0.8.3 | | :x: | -| 0.7.0 | | :x: | -| 0.6.0 | | :x: | -| 0.5.4 | | :x: | -| 0.4.1 | | :x: | -| 0.3.1 | | :x: | -| 0.2.0 | | :x: | -| 0.1.9 | | :x: | -:white_check_mark: = security/bug fixes -:heavy_check_mark: = full support +| Version | LTS | Supported | +| ------- | ------------- | ----------------------- | +| 22.6 | | :white_check_mark: | +| 22.3 | | :x: | +| 21.12 | until 2023-12 | :white_check_mark: | +| 21.9 | | :x: | +| 21.6 | | :x: | +| 21.3 | | :x: | +| 20.12 | until 2022-12 | :ballot_box_with_check: | +| 20.9 | | :x: | +| 20.6 | | :x: | +| 20.3 | | :x: | +| 19.12 | | :x: | +| 19.9 | | :x: | +| 19.6 | | :x: | +| 19.3 | | :x: | +| 18.12 | | :x: | +| 0.8.3 | | :x: | +| 0.7.0 | | :x: | +| 0.6.0 | | :x: | +| 0.5.4 | | :x: | +| 0.4.1 | | :x: | +| 0.3.1 | | :x: | +| 0.2.0 | | :x: | +| 0.1.9 | | :x: | + +:ballot_box_with_check: = security/bug fixes +:white_check_mark: = full support ## Reporting a Vulnerability If you discover a security vulnerability, we ask that you **do not** create an issue on GitHub. Instead, please [send a message to the core-devs](https://community.sanicframework.org/g/core-devs) on the community forums. Once logged in, you can send a message to the core-devs by clicking the message button. +Alternatively, you can send a private message to Adam Hopkins on Discord. Find him on the [Sanic discord server](https://discord.gg/FARQzAEMAA). + This will help to not publicize the issue until the team can address it and resolve it. diff --git a/sanic/app.py b/sanic/app.py index 4926edd4..237f818c 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1308,7 +1308,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): self.config.update_config(config) @property - def asgi(self): + def asgi(self) -> bool: return self.state.asgi @asgi.setter @@ -1515,6 +1515,18 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta): self.signalize(self.config.TOUCHUP) self.finalize() + route_names = [route.name for route in self.router.routes] + duplicates = { + name for name in route_names if route_names.count(name) > 1 + } + if duplicates: + names = ", ".join(duplicates) + deprecation( + f"Duplicate route names detected: {names}. In the future, " + "Sanic will enforce uniqueness in route naming.", + 23.3, + ) + # TODO: Replace in v22.6 to check against apps in app registry if ( self.__class__._uvloop_setting is not None diff --git a/sanic/constants.py b/sanic/constants.py index 52ec50ef..988d8bae 100644 --- a/sanic/constants.py +++ b/sanic/constants.py @@ -34,6 +34,15 @@ class LocalCertCreator(str, Enum): HTTP_METHODS = tuple(HTTPMethod.__members__.values()) +SAFE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD, HTTPMethod.OPTIONS) +IDEMPOTENT_HTTP_METHODS = ( + HTTPMethod.GET, + HTTPMethod.HEAD, + HTTPMethod.PUT, + HTTPMethod.DELETE, + HTTPMethod.OPTIONS, +) +CACHEABLE_HTTP_METHODS = (HTTPMethod.GET, HTTPMethod.HEAD) DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" DEFAULT_LOCAL_TLS_KEY = "key.pem" DEFAULT_LOCAL_TLS_CERT = "cert.pem" diff --git a/sanic/mixins/runner.py b/sanic/mixins/runner.py index 3d2b327d..0387b095 100644 --- a/sanic/mixins/runner.py +++ b/sanic/mixins/runner.py @@ -525,7 +525,7 @@ class RunnerMixin(metaclass=SanicMeta): ) ) else: - server = "" + server = "ASGI" if self.asgi else "unknown" # type: ignore display = { "mode": " ".join(mode), @@ -571,8 +571,12 @@ class RunnerMixin(metaclass=SanicMeta): @property def serve_location(self) -> str: - server_settings = self.state.server_info[0].settings - return self.get_server_location(server_settings) + try: + server_settings = self.state.server_info[0].settings + return self.get_server_location(server_settings) + except IndexError: + location = "ASGI" if self.asgi else "unknown" # type: ignore + return f"http://<{location}>" @staticmethod def get_server_location( diff --git a/sanic/request.py b/sanic/request.py index 17016c97..ae872d10 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -38,7 +38,12 @@ from httptools import parse_url from httptools.parser.errors import HttpParserInvalidURLError from sanic.compat import CancelledErrors, Header -from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE +from sanic.constants import ( + CACHEABLE_HTTP_METHODS, + DEFAULT_HTTP_CONTENT_TYPE, + IDEMPOTENT_HTTP_METHODS, + SAFE_HTTP_METHODS, +) from sanic.exceptions import BadRequest, BadURL, ServerError from sanic.headers import ( AcceptContainer, @@ -986,6 +991,33 @@ class Request: return self.transport.scope + @property + def is_safe(self) -> bool: + """ + :return: Whether the HTTP method is safe. + See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1 + :rtype: bool + """ + return self.method in SAFE_HTTP_METHODS + + @property + def is_idempotent(self) -> bool: + """ + :return: Whether the HTTP method is iempotent. + See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2 + :rtype: bool + """ + return self.method in IDEMPOTENT_HTTP_METHODS + + @property + def is_cacheable(self) -> bool: + """ + :return: Whether the HTTP method is cacheable. + See https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.3 + :rtype: bool + """ + return self.method in CACHEABLE_HTTP_METHODS + class File(NamedTuple): """ diff --git a/tests/test_asgi.py b/tests/test_asgi.py index 4c473047..129bbe0b 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -546,3 +546,13 @@ async def test_signals_triggered(app): assert response.status_code == 200 assert response.text == "test_signals_triggered" assert signals_triggered == signals_expected + + +@pytest.mark.asyncio +async def test_asgi_serve_location(app): + @app.get("/") + def _request(request: Request): + return text(request.app.serve_location) + + _, response = await app.asgi_client.get("/") + assert response.text == "http://" diff --git a/tests/test_http.py b/tests/test_http.py index 1ca162e1..f4e11e0e 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -76,7 +76,7 @@ def test_full_message(client): ) response = client.recv() - # AltSvcCheck touchup removes the Alt-Svc header from the + # AltSvcCheck touchup removes the Alt-Svc header from the # response in the Python 3.9+ in this case assert len(response) == (151 if version_info < (3, 9) else 140) assert b"200 OK" in response diff --git a/tests/test_request.py b/tests/test_request.py index e01cc1e7..3a4132ae 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -243,3 +243,54 @@ def test_request_stream_id(app): _, resp = app.test_client.get("/") assert resp.text == "Stream ID is only a property of a HTTP/3 request" + + +@pytest.mark.parametrize( + "method,safe", + ( + ("DELETE", False), + ("GET", True), + ("HEAD", True), + ("OPTIONS", True), + ("PATCH", False), + ("POST", False), + ("PUT", False), + ), +) +def test_request_safe(method, safe): + request = Request(b"/", {}, None, method, None, None) + assert request.is_safe is safe + + +@pytest.mark.parametrize( + "method,idempotent", + ( + ("DELETE", True), + ("GET", True), + ("HEAD", True), + ("OPTIONS", True), + ("PATCH", False), + ("POST", False), + ("PUT", True), + ), +) +def test_request_idempotent(method, idempotent): + request = Request(b"/", {}, None, method, None, None) + assert request.is_idempotent is idempotent + + +@pytest.mark.parametrize( + "method,cacheable", + ( + ("DELETE", False), + ("GET", True), + ("HEAD", True), + ("OPTIONS", False), + ("PATCH", False), + ("POST", False), + ("PUT", False), + ), +) +def test_request_cacheable(method, cacheable): + request = Request(b"/", {}, None, method, None, None) + assert request.is_cacheable is cacheable diff --git a/tests/test_routes.py b/tests/test_routes.py index 6fdf97d5..4ff5d9fd 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -1266,3 +1266,22 @@ async def test_added_callable_route_ctx_kwargs(app): assert request.route.ctx.foo() == "foo" assert await request.route.ctx.bar() == 99 + + +@pytest.mark.asyncio +async def test_duplicate_route_deprecation(app): + @app.route("/foo", name="duped") + async def handler_foo(request): + return text("...") + + @app.route("/bar", name="duped") + async def handler_bar(request): + return text("...") + + message = ( + r"\[DEPRECATION v23\.3\] Duplicate route names detected: " + r"test_duplicate_route_deprecation\.duped\. In the future, " + r"Sanic will enforce uniqueness in route naming\." + ) + with pytest.warns(DeprecationWarning, match=message): + await app._startup()