Unify response header processing of ASGI and asyncio modes.

This commit is contained in:
L. Kärkkäinen 2020-03-08 17:33:56 +02:00
parent d348bb4ff4
commit 5351cda979
4 changed files with 21 additions and 40 deletions

View File

@ -281,7 +281,7 @@ class ASGIApp:
await self.transport.send({
"type": "http.response.start",
"status": response.status,
"headers": response.full_headers,
"headers": response.processed_headers,
})
response_body = getattr(response, "body", None)
if response_body:

View File

@ -7,6 +7,7 @@ from sanic.helpers import STATUS_CODES
HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str
HeaderBytesIterable = Iterable[Tuple[bytes, bytes]]
Options = Dict[str, Union[int, str]] # key=value fields in various headers
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
@ -175,26 +176,13 @@ def parse_host(host: str) -> Tuple[Optional[str], Optional[int]]:
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()
def format_http1_response(
status: int, headers: HeaderIterable, body=b""
status: int, headers: HeaderBytesIterable, body=b""
) -> bytes:
"""Format a full HTTP/1.1 response.
- If `body` is included, content-length must be specified in headers.
"""
headerbytes = format_http1(headers)
"""Format a full HTTP/1.1 response."""
return b"HTTP/1.1 %d %b\r\n%b\r\n%b" % (
status,
STATUS_CODES.get(status, b"UNKNOWN"),
headerbytes,
b"".join(b"%b: %b\r\n" % h for h in headers),
body,
)

View File

@ -10,7 +10,7 @@ from sanic.exceptions import (
ServiceUnavailable,
)
from sanic.headers import format_http1_response
from sanic.helpers import has_message_body, remove_entity_headers
from sanic.helpers import has_message_body
from sanic.log import access_logger, logger
@ -182,14 +182,9 @@ class Http:
data, end_stream = res.body, True
size = len(data)
headers = res.headers
if res.content_type and "content-type" not in headers:
headers["content-type"] = res.content_type
status = res.status
if not isinstance(status, int) or status < 200:
raise RuntimeError(f"Invalid response status {status!r}")
# Not Modified, Precondition Failed
if status in (304, 412):
headers = remove_entity_headers(headers)
if not has_message_body(status):
# Header-only response status
self.response_func = None
@ -227,7 +222,7 @@ class Http:
data = b""
self.response_func = self.head_response_ignored
headers["connection"] = "keep-alive" if self.keep_alive else "close"
ret = format_http1_response(status, headers.items(), data)
ret = format_http1_response(status, res.processed_headers, data)
# Send a 100-continue if expected and not Expectation Failed
if self.expecting_continue:
self.expecting_continue = False

View File

@ -7,8 +7,7 @@ from urllib.parse import quote_plus
from sanic.compat import Header, open_async
from sanic.cookies import CookieJar
from sanic.headers import format_http1
from sanic.helpers import has_message_body
from sanic.helpers import has_message_body, remove_entity_headers
try:
@ -25,9 +24,6 @@ class BaseHTTPResponse:
def _encode_body(self, data):
return data.encode() if hasattr(data, "encode") else data
def _parse_headers(self):
return format_http1(self.full_headers)
@property
def cookies(self):
if self._cookies is None:
@ -35,14 +31,22 @@ class BaseHTTPResponse:
return self._cookies
@property
def full_headers(self):
"""Obtain an encoded tuple of headers for a response to be sent."""
def processed_headers(self):
"""Obtain a list of header tuples encoded in bytes for sending.
Add and remove headers based on status and content_type.
"""
headers = []
cookies = {}
if self.content_type and not "content-type" in self.headers:
headers += (b"content-type", self.content_type.encode()),
status = self.status
# TODO: Make a blacklist set of header names and then filter with that
if status in (304, 412): # Not Modified, Precondition Failed
self.headers = remove_entity_headers(self.headers)
if has_message_body(status):
if self.content_type and not "content-type" in self.headers:
headers += (b"content-type", self.content_type.encode()),
for name, value in self.headers.items():
name = f"{name}"
name = f"{name}".lower()
if name.lower() == "set-cookie":
cookies[value.key] = value
else:
@ -126,12 +130,6 @@ class HTTPResponse(BaseHTTPResponse):
self.headers = Header(headers or {})
self._cookies = None
@property
def cookies(self):
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies
def empty(status=204, headers=None):
"""