Unify response header processing of ASGI and asyncio modes.
This commit is contained in:
		| @@ -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: | ||||
|   | ||||
| @@ -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, | ||||
|     ) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 = {} | ||||
|         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): | ||||
|     """ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 L. Kärkkäinen
					L. Kärkkäinen