Unify response header processing of ASGI and asyncio modes.
This commit is contained in:
parent
d348bb4ff4
commit
5351cda979
|
@ -281,7 +281,7 @@ class ASGIApp:
|
||||||
await self.transport.send({
|
await self.transport.send({
|
||||||
"type": "http.response.start",
|
"type": "http.response.start",
|
||||||
"status": response.status,
|
"status": response.status,
|
||||||
"headers": response.full_headers,
|
"headers": response.processed_headers,
|
||||||
})
|
})
|
||||||
response_body = getattr(response, "body", None)
|
response_body = getattr(response, "body", None)
|
||||||
if response_body:
|
if response_body:
|
||||||
|
|
|
@ -7,6 +7,7 @@ from sanic.helpers import STATUS_CODES
|
||||||
|
|
||||||
|
|
||||||
HeaderIterable = Iterable[Tuple[str, Any]] # Values convertible to str
|
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
|
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
|
||||||
|
|
||||||
|
@ -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
|
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(
|
def format_http1_response(
|
||||||
status: int, headers: HeaderIterable, body=b""
|
status: int, headers: HeaderBytesIterable, body=b""
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""Format a full HTTP/1.1 response.
|
"""Format a full HTTP/1.1 response."""
|
||||||
|
|
||||||
- If `body` is included, content-length must be specified in headers.
|
|
||||||
"""
|
|
||||||
headerbytes = format_http1(headers)
|
|
||||||
return b"HTTP/1.1 %d %b\r\n%b\r\n%b" % (
|
return b"HTTP/1.1 %d %b\r\n%b\r\n%b" % (
|
||||||
status,
|
status,
|
||||||
STATUS_CODES.get(status, b"UNKNOWN"),
|
STATUS_CODES.get(status, b"UNKNOWN"),
|
||||||
headerbytes,
|
b"".join(b"%b: %b\r\n" % h for h in headers),
|
||||||
body,
|
body,
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,7 +10,7 @@ from sanic.exceptions import (
|
||||||
ServiceUnavailable,
|
ServiceUnavailable,
|
||||||
)
|
)
|
||||||
from sanic.headers import format_http1_response
|
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
|
from sanic.log import access_logger, logger
|
||||||
|
|
||||||
|
|
||||||
|
@ -182,14 +182,9 @@ class Http:
|
||||||
data, end_stream = res.body, True
|
data, end_stream = res.body, True
|
||||||
size = len(data)
|
size = len(data)
|
||||||
headers = res.headers
|
headers = res.headers
|
||||||
if res.content_type and "content-type" not in headers:
|
|
||||||
headers["content-type"] = res.content_type
|
|
||||||
status = res.status
|
status = res.status
|
||||||
if not isinstance(status, int) or status < 200:
|
if not isinstance(status, int) or status < 200:
|
||||||
raise RuntimeError(f"Invalid response status {status!r}")
|
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):
|
if not has_message_body(status):
|
||||||
# Header-only response status
|
# Header-only response status
|
||||||
self.response_func = None
|
self.response_func = None
|
||||||
|
@ -227,7 +222,7 @@ class Http:
|
||||||
data = b""
|
data = b""
|
||||||
self.response_func = self.head_response_ignored
|
self.response_func = self.head_response_ignored
|
||||||
headers["connection"] = "keep-alive" if self.keep_alive else "close"
|
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
|
# Send a 100-continue if expected and not Expectation Failed
|
||||||
if self.expecting_continue:
|
if self.expecting_continue:
|
||||||
self.expecting_continue = False
|
self.expecting_continue = False
|
||||||
|
|
|
@ -7,8 +7,7 @@ from urllib.parse import quote_plus
|
||||||
|
|
||||||
from sanic.compat import Header, open_async
|
from sanic.compat import Header, open_async
|
||||||
from sanic.cookies import CookieJar
|
from sanic.cookies import CookieJar
|
||||||
from sanic.headers import format_http1
|
from sanic.helpers import has_message_body, remove_entity_headers
|
||||||
from sanic.helpers import has_message_body
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -25,9 +24,6 @@ class BaseHTTPResponse:
|
||||||
def _encode_body(self, data):
|
def _encode_body(self, data):
|
||||||
return data.encode() if hasattr(data, "encode") else data
|
return data.encode() if hasattr(data, "encode") else data
|
||||||
|
|
||||||
def _parse_headers(self):
|
|
||||||
return format_http1(self.full_headers)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookies(self):
|
def cookies(self):
|
||||||
if self._cookies is None:
|
if self._cookies is None:
|
||||||
|
@ -35,14 +31,22 @@ class BaseHTTPResponse:
|
||||||
return self._cookies
|
return self._cookies
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_headers(self):
|
def processed_headers(self):
|
||||||
"""Obtain an encoded tuple of headers for a response to be sent."""
|
"""Obtain a list of header tuples encoded in bytes for sending.
|
||||||
|
|
||||||
|
Add and remove headers based on status and content_type.
|
||||||
|
"""
|
||||||
headers = []
|
headers = []
|
||||||
cookies = {}
|
cookies = {}
|
||||||
|
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:
|
if self.content_type and not "content-type" in self.headers:
|
||||||
headers += (b"content-type", self.content_type.encode()),
|
headers += (b"content-type", self.content_type.encode()),
|
||||||
for name, value in self.headers.items():
|
for name, value in self.headers.items():
|
||||||
name = f"{name}"
|
name = f"{name}".lower()
|
||||||
if name.lower() == "set-cookie":
|
if name.lower() == "set-cookie":
|
||||||
cookies[value.key] = value
|
cookies[value.key] = value
|
||||||
else:
|
else:
|
||||||
|
@ -126,12 +130,6 @@ class HTTPResponse(BaseHTTPResponse):
|
||||||
self.headers = Header(headers or {})
|
self.headers = Header(headers or {})
|
||||||
self._cookies = None
|
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):
|
def empty(status=204, headers=None):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue
Block a user