From 7827b1b41d36dcf297c8bf8a3e8b74d5368339a2 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 10 Aug 2022 21:12:09 +0300 Subject: [PATCH] Add Request properties for HTTP method info (#2516) --- sanic/constants.py | 9 ++++++++ sanic/request.py | 34 ++++++++++++++++++++++++++++- tests/test_request.py | 51 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) 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/request.py b/sanic/request.py index 927c124a..0a8ca59e 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, @@ -975,6 +980,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_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