Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add raw header info to request object #2032

Merged
merged 8 commits into from
Mar 3, 2021
1 change: 1 addition & 0 deletions sanic/constants.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
HTTP_METHODS = ("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE")
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
8 changes: 5 additions & 3 deletions sanic/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -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"):
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions sanic/mixins/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
Expand Down
20 changes: 14 additions & 6 deletions sanic/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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):
"""
Expand Down Expand Up @@ -95,6 +90,7 @@ class Request:
"conn_info",
"ctx",
"endpoint",
"head",
"headers",
"method",
"name",
Expand All @@ -121,6 +117,7 @@ def __init__(
method: str,
transport: TransportProtocol,
app: Sanic,
head: bytes = b"",
):
self.raw_url = url_bytes
# TODO: Content-Encoding detection
Expand All @@ -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""
Expand Down Expand Up @@ -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]]:
"""
Expand Down
3 changes: 2 additions & 1 deletion sanic/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
36 changes: 33 additions & 3 deletions tests/test_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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"