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.
This commit is contained in:
L. Kärkkäinen 2019-12-24 01:30:45 +02:00 committed by Stephen Sadowski
parent fccbc1adc4
commit 0a25868a86
3 changed files with 18 additions and 16 deletions

View File

@ -130,6 +130,10 @@ class Cookie(dict):
:return: Cookie encoded in a codec of choosing. :return: Cookie encoded in a codec of choosing.
:except: UnicodeEncodeError :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))] output = ["%s=%s" % (self.key, _quote(self.value))]
for key, value in self.items(): for key, value in self.items():
if key == "max-age": if key == "max-age":
@ -147,4 +151,4 @@ class Cookie(dict):
else: else:
output.append("%s=%s" % (self._keys[key], value)) output.append("%s=%s" % (self._keys[key], value))
return "; ".join(output).encode(encoding) return "; ".join(output)

View File

@ -1,9 +1,10 @@
import re 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 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 Options = Dict[str, Union[int, str]] # key=value fields in various headers
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys 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 return None, None
host, port = m.groups() host, port = m.groups()
return host.lower(), int(port) if port is not None else None 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()

View File

@ -7,6 +7,7 @@ from aiofiles import open as open_async # type: ignore
from sanic.compat import Header from sanic.compat import Header
from sanic.cookies import CookieJar from sanic.cookies import CookieJar
from sanic.headers import format_http1
from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers from sanic.helpers import STATUS_CODES, has_message_body, remove_entity_headers
@ -30,20 +31,7 @@ class BaseHTTPResponse:
return str(data).encode() return str(data).encode()
def _parse_headers(self): def _parse_headers(self):
headers = b"" return format_http1(self.headers.items())
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
@property @property
def cookies(self): def cookies(self):