From bb65dda2776546c04edf445472b1e85a47d00e45 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 3 Mar 2021 09:21:44 +0200 Subject: [PATCH 1/3] Docs changes for RTD --- docs/conf.py | 5 +++++ docs/index.rst | 8 ++++++++ docs/sanic/api_reference.rst | 4 ++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index eeaa91f491..b1d754ee0d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -174,3 +174,8 @@ def setup(app): True, ) app.add_transform(AutoStructify) + + +html_theme_options = { + "style_external_links": True, +} diff --git a/docs/index.rst b/docs/index.rst index d714dc4d3b..ff6bf6c7f4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,3 +1,8 @@ +User Guide +========== + +To learn about using Sanic, checkout the `User Guide `_. + .. include:: ../README.rst API @@ -6,7 +11,10 @@ API .. toctree:: :maxdepth: 4 + 👥 User Guide sanic/api_reference + 💻 Source code + ❓ Support Module Documentation diff --git a/docs/sanic/api_reference.rst b/docs/sanic/api_reference.rst index 796148f749..df369bc2d5 100644 --- a/docs/sanic/api_reference.rst +++ b/docs/sanic/api_reference.rst @@ -1,5 +1,5 @@ -API Reference -============= +📑 API Reference +================ sanic.app --------- From c41d7136e820c4340bde00b482d2efcea98c6407 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 3 Mar 2021 09:26:22 +0200 Subject: [PATCH 2/3] Change order of Docs index --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index ff6bf6c7f4..a3ec5caebc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,10 +1,10 @@ +.. include:: ../README.rst + User Guide ========== To learn about using Sanic, checkout the `User Guide `_. -.. include:: ../README.rst - API ====== From a733d3271536682d2e08cdcf8e741f11c8ffc371 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 3 Mar 2021 16:33:34 +0200 Subject: [PATCH 3/3] Add raw header info to request object (#2032) --- sanic/constants.py | 1 + sanic/http.py | 8 +++++--- sanic/mixins/routes.py | 4 ++-- sanic/request.py | 20 ++++++++++++++------ sanic/response.py | 3 ++- tests/test_headers.py | 36 +++++++++++++++++++++++++++++++++--- 6 files changed, 57 insertions(+), 15 deletions(-) diff --git a/sanic/constants.py b/sanic/constants.py index 8bd87fe90d..cb2e8ffa97 100644 --- a/sanic/constants.py +++ b/sanic/constants.py @@ -1 +1,2 @@ HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE") +DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" diff --git a/sanic/http.py b/sanic/http.py index 066411cbb9..0303d4cfa5 100644 --- a/sanic/http.py +++ b/sanic/http.py @@ -189,8 +189,9 @@ async def http1_request_header(self): # Parse header content try: - raw_headers = buf[:pos].decode(errors="surrogateescape") - reqline, *raw_headers = raw_headers.split("\r\n") + head = buf[:pos] + raw_headers = head.decode(errors="surrogateescape") + reqline, *split_headers = raw_headers.split("\r\n") method, self.url, protocol = reqline.split(" ") if protocol == "HTTP/1.1": @@ -204,7 +205,7 @@ async def http1_request_header(self): request_body = False headers = [] - for name, value in (h.split(":", 1) for h in raw_headers): + for name, value in (h.split(":", 1) for h in split_headers): name, value = h = name.lower(), value.lstrip() if name in ("content-length", "transfer-encoding"): @@ -223,6 +224,7 @@ async def http1_request_header(self): request = self.protocol.request_class( url_bytes=self.url.encode(), headers=headers_instance, + head=bytes(head), version=protocol[5:], method=method, transport=self.protocol.transport, diff --git a/sanic/mixins/routes.py b/sanic/mixins/routes.py index 060fd3272f..a8451ab275 100644 --- a/sanic/mixins/routes.py +++ b/sanic/mixins/routes.py @@ -11,7 +11,7 @@ from sanic_routing.route import Route # type: ignore from sanic.compat import stat_async -from sanic.constants import HTTP_METHODS +from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS from sanic.exceptions import ( ContentRangeError, FileNotFound, @@ -689,7 +689,7 @@ async def _static_request_handler( content_type = ( content_type or guess_type(file_path)[0] - or "application/octet-stream" + or DEFAULT_HTTP_CONTENT_TYPE ) if "charset=" not in content_type and ( diff --git a/sanic/request.py b/sanic/request.py index f50e1e5cae..6296419e21 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -31,6 +31,7 @@ from httptools import parse_url # type: ignore from sanic.compat import CancelledErrors, Header +from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE from sanic.exceptions import InvalidUsage from sanic.headers import ( Options, @@ -49,12 +50,6 @@ except ImportError: from json import loads as json_loads # type: ignore -DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" - -# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 -# > If the media type remains unknown, the recipient SHOULD treat it -# > as type "application/octet-stream" - class RequestParameters(dict): """ @@ -95,6 +90,7 @@ class Request: "conn_info", "ctx", "endpoint", + "head", "headers", "method", "name", @@ -121,6 +117,7 @@ def __init__( method: str, transport: TransportProtocol, app: Sanic, + head: bytes = b"", ): self.raw_url = url_bytes # TODO: Content-Encoding detection @@ -132,6 +129,7 @@ def __init__( self.version = version self.method = method self.transport = transport + self.head = head # Init but do not inhale self.body = b"" @@ -207,6 +205,16 @@ async def receive_body(self): if not self.body: self.body = b"".join([data async for data in self.stream]) + @property + def raw_headers(self): + _, headers = self.head.split(b"\r\n", 1) + return bytes(headers) + + @property + def request_line(self): + reqline, _ = self.head.split(b"\r\n", 1) + return bytes(reqline) + @property def id(self) -> Optional[Union[uuid.UUID, str, int]]: """ diff --git a/sanic/response.py b/sanic/response.py index 10070aa2b6..e17b080d2d 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -17,6 +17,7 @@ from warnings import warn from sanic.compat import Header, open_async +from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE from sanic.cookies import CookieJar from sanic.helpers import has_message_body, remove_entity_headers from sanic.http import Http @@ -297,7 +298,7 @@ def raw( body: Optional[AnyStr], status: int = 200, headers: Optional[Dict[str, str]] = None, - content_type: str = "application/octet-stream", + content_type: str = DEFAULT_HTTP_CONTENT_TYPE, ) -> HTTPResponse: """ Returns response object without encoding the body. diff --git a/tests/test_headers.py b/tests/test_headers.py index 7d552fb86a..4580a073d9 100644 --- a/tests/test_headers.py +++ b/tests/test_headers.py @@ -2,11 +2,9 @@ import pytest -from sanic import Sanic, headers -from sanic.compat import Header +from sanic import headers, text from sanic.exceptions import PayloadTooLarge from sanic.http import Http -from sanic.request import Request @pytest.mark.parametrize( @@ -85,3 +83,35 @@ async def _receive_more(): with pytest.raises(PayloadTooLarge): await http.http1_request_header() + + +def test_raw_headers(app): + app.route("/")(lambda _: text("")) + request, _ = app.test_client.get( + "/", + headers={ + "FOO": "bar", + "Host": "example.com", + "User-Agent": "Sanic-Testing", + }, + ) + + assert request.raw_headers == ( + b"Host: example.com\r\nAccept: */*\r\nAccept-Encoding: gzip, " + b"deflate\r\nConnection: keep-alive\r\nUser-Agent: " + b"Sanic-Testing\r\nFOO: bar" + ) + + +def test_request_line(app): + app.route("/")(lambda _: text("")) + request, _ = app.test_client.get( + "/", + headers={ + "FOO": "bar", + "Host": "example.com", + "User-Agent": "Sanic-Testing", + }, + ) + + assert request.request_line == b"GET / HTTP/1.1"