From 0a25868a86074aacff099fb2dee65da425ba52e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=2E=20K=C3=A4rkk=C3=A4inen?= <98187+Tronic@users.noreply.github.com> Date: Tue, 24 Dec 2019 01:30:45 +0200 Subject: [PATCH] HTTP1 header formatting moved to headers.format_headers and rewritten. (#1669) * HTTP1 header formatting moved to headers.format_headers and rewritten. - New implementation is one line of code and twice faster than the old one. - Whole header block encoded to UTF-8 in one pass. - No longer supports custom encode method on header values. - Cookie objects now have __str__ in addition to encode, to work with this. * Add an import missed in merge. --- sanic/cookies.py | 6 +++++- sanic/headers.py | 12 +++++++++++- sanic/response.py | 16 ++-------------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/sanic/cookies.py b/sanic/cookies.py index 19907945..ed672fba 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -130,6 +130,10 @@ class Cookie(dict): :return: Cookie encoded in a codec of choosing. :except: UnicodeEncodeError """ + return str(self).encode(encoding) + + def __str__(self): + """Format as a Set-Cookie header value.""" output = ["%s=%s" % (self.key, _quote(self.value))] for key, value in self.items(): if key == "max-age": @@ -147,4 +151,4 @@ class Cookie(dict): else: output.append("%s=%s" % (self._keys[key], value)) - return "; ".join(output).encode(encoding) + return "; ".join(output) diff --git a/sanic/headers.py b/sanic/headers.py index 142ab27b..eef468f9 100644 --- a/sanic/headers.py +++ b/sanic/headers.py @@ -1,9 +1,10 @@ import re -from typing import Dict, Iterable, List, Optional, Tuple, Union +from typing import Any, Dict, Iterable, List, Optional, Tuple, Union from urllib.parse import unquote +HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str Options = Dict[str, Union[int, str]] # key=value fields in various headers OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys @@ -170,3 +171,12 @@ def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]: return None, None host, port = m.groups() return host.lower(), int(port) if port is not None else None + + +def format_http1(headers: HeaderIterable) -> bytes: + """Convert a headers iterable into HTTP/1 header format. + + - Outputs UTF-8 bytes where each header line ends with \\r\\n. + - Values are converted into strings if necessary. + """ + return "".join(f"{name}: {val}\r\n" for name, val in headers).encode() diff --git a/sanic/response.py b/sanic/response.py index c75af62e..0b92d2bd 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -7,6 +7,7 @@ from aiofiles import open as open_async # type: ignore from sanic.compat import Header from sanic.cookies import CookieJar +from sanic.headers import format_http1 from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers @@ -30,20 +31,7 @@ class BaseHTTPResponse: return str(data).encode() def _parse_headers(self): - headers = b"" - for name, value in self.headers.items(): - try: - headers += b"%b: %b\r\n" % ( - name.encode(), - value.encode("utf-8"), - ) - except AttributeError: - headers += b"%b: %b\r\n" % ( - str(name).encode(), - str(value).encode("utf-8"), - ) - - return headers + return format_http1(self.headers.items()) @property def cookies(self):