Refactoring and cleanup.

This commit is contained in:
L. Kärkkäinen 2020-03-08 16:56:22 +02:00
parent 990ac52a1a
commit d348bb4ff4
4 changed files with 72 additions and 121 deletions

View File

@ -272,84 +272,20 @@ class ASGIApp:
yield data
def respond(self, response):
headers: List[Tuple[bytes, bytes]] = []
cookies: Dict[str, str] = {}
try:
cookies = {
v.key: v
for _, v in list(
filter(
lambda item: item[0].lower() == "set-cookie",
response.headers.items(),
)
)
}
headers += [
(str(name).encode("latin-1"), str(value).encode("latin-1"))
for name, value in response.headers.items()
if name.lower() not in ["set-cookie"]
]
except AttributeError:
logger.error(
"Invalid response object for url %s, "
"Expected Type: HTTPResponse, Actual Type: %s",
self.request.url,
type(response),
)
exception = ServerError("Invalid response type")
response = self.sanic_app.error_handler.response(
self.request, exception
)
headers = [
(str(name).encode("latin-1"), str(value).encode("latin-1"))
for name, value in response.headers.items()
if name not in (b"Set-Cookie",)
]
response.stream, self.response = self, response
return response
if "content-length" not in response.headers and not isinstance(
response, StreamingHTTPResponse
):
headers += [
(b"content-length", str(len(response.body)).encode("latin-1"))
]
if "content-type" not in response.headers:
headers += [
(b"content-type", str(response.content_type).encode("latin-1"))
]
if response.cookies:
cookies.update(
{
v.key: v
for _, v in response.cookies.items()
if v.key not in cookies.keys()
}
)
headers += [
(b"set-cookie", cookie.encode("utf-8"))
for k, cookie in cookies.items()
]
self.response_start = {
"type": "http.response.start",
"status": response.status,
"headers": headers,
}
self.response_body = response.body
return self
async def send(self, data=None, end_stream=None):
if data is None is end_stream:
end_stream = True
if self.response_start:
await self.transport.send(self.response_start)
self.response_start = None
if self.response_body:
data = (
self.response_body + data if data else self.response_body
)
self.response_body = None
async def send(self, data, end_stream):
if self.response:
response, self.response = self.response, None
await self.transport.send({
"type": "http.response.start",
"status": response.status,
"headers": response.full_headers,
})
response_body = getattr(response, "body", None)
if response_body:
data = response_body + data if data else response_body
await self.transport.send(
{
"type": "http.response.body",

View File

@ -77,7 +77,7 @@ class Http:
if self.stage is Stage.HANDLER:
raise ServerError("Handler produced no response")
if self.stage is Stage.RESPONSE:
await self.send(end_stream=True)
await self.response.send(end_stream=True)
except CancelledError:
# Write an appropriate response before exiting
e = self.exception or ServiceUnavailable(f"Cancelled")
@ -178,13 +178,15 @@ class Http:
def http1_response_header(self, data, end_stream) -> bytes:
res = self.response
# Compatibility with simple response body
if not data and res.body:
if not data and getattr(res, "body", None):
data, end_stream = res.body, True
size = len(data)
status = res.status
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)
@ -374,19 +376,10 @@ class Http:
if self.stage is not Stage.HANDLER:
self.stage = Stage.FAILED
raise RuntimeError("Response already started")
if not isinstance(response.status, int) or response.status < 200:
raise RuntimeError(f"Invalid response status {response.status!r}")
self.response = response
return self
self.response, response.stream = response, self
return response
async def send(self, data=None, end_stream=None):
"""Send any pending response headers and the given data as body.
:param data: str or bytes to be written
:end_stream: whether to close the stream after this block
"""
if data is None and end_stream is None:
end_stream = True
data = data.encode() if hasattr(data, "encode") else data or b""
async def send(self, data, end_stream):
data = self.response_func(data, end_stream)
if not data:
return

View File

@ -106,15 +106,17 @@ class Request:
)
def respond(
self, status=200, headers=None, content_type=DEFAULT_HTTP_CONTENT_TYPE
self, response=None, *, status=200, headers=None, content_type=None
):
return self.stream.respond(
status
if isinstance(status, HTTPResponse)
else HTTPResponse(
status=status, headers=headers, content_type=content_type,
# This logic of determining which response to use is subject to change
if response is None:
response = self.stream.response or HTTPResponse(
status=status,
headers=headers,
content_type=content_type,
)
)
# Connect the response and return it
return self.stream.respond(response)
async def receive_body(self):
self.body = b"".join([data async for data in self.stream])

View File

@ -26,7 +26,7 @@ class BaseHTTPResponse:
return data.encode() if hasattr(data, "encode") else data
def _parse_headers(self):
return format_http1(self.headers.items())
return format_http1(self.full_headers)
@property
def cookies(self):
@ -34,6 +34,43 @@ class BaseHTTPResponse:
self._cookies = CookieJar(self.headers)
return self._cookies
@property
def full_headers(self):
"""Obtain an encoded tuple of headers for a response to be sent."""
headers = []
cookies = {}
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}"
if name.lower() == "set-cookie":
cookies[value.key] = value
else:
headers += (name.encode("ascii"), f"{value}".encode()),
if self.cookies:
cookies.update(
(v.key, v)
for v in self.cookies.values()
if v.key not in cookies
)
headers += [
(b"set-cookie", cookie.encode("utf-8"))
for k, cookie in cookies.items()
]
return headers
async def send(self, data=None, end_stream=None):
"""Send any pending response headers and the given data as body.
:param data: str or bytes to be written
:end_stream: whether to close the stream after this block
"""
if data is None and end_stream is None:
end_stream = True
data = data.encode() if hasattr(data, "encode") else data or b""
await self.stream.send(data, end_stream=end_stream)
class StreamingHTTPResponse(BaseHTTPResponse):
__slots__ = (
@ -42,9 +79,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
"status",
"content_type",
"headers",
"chunked",
"_cookies",
"send",
)
def __init__(
@ -53,13 +88,12 @@ class StreamingHTTPResponse(BaseHTTPResponse):
status=200,
headers=None,
content_type="text/plain; charset=utf-8",
chunked=True,
chunked="deprecated",
):
self.content_type = content_type
self.streaming_fn = streaming_fn
self.status = status
self.headers = Header(headers or {})
self.chunked = chunked
self._cookies = None
async def write(self, data):
@ -70,9 +104,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
await self.send(self._encode_body(data))
async def stream(self, request):
self.send = request.respond(
self.status, self.headers, self.content_type,
).send
request.respond(self)
await self.streaming_fn(self)
await self.send(end_stream=True)
@ -94,16 +126,6 @@ class HTTPResponse(BaseHTTPResponse):
self.headers = Header(headers or {})
self._cookies = None
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
body = b""
if has_message_body(self.status):
body = self.body
self.headers["Content-Length"] = self.headers.get(
"Content-Length", len(self.body)
)
return self.get_headers(version, keep_alive, keep_alive_timeout, body)
@property
def cookies(self):
if self._cookies is None:
@ -265,7 +287,7 @@ async def file_stream(
mime_type=None,
headers=None,
filename=None,
chunked=True,
chunked="deprecated",
_range=None,
):
"""Return a streaming response object with file data.
@ -314,7 +336,6 @@ async def file_stream(
status=status,
headers=headers,
content_type=mime_type,
chunked=chunked,
)
@ -323,7 +344,7 @@ def stream(
status=200,
headers=None,
content_type="text/plain; charset=utf-8",
chunked=True,
chunked="deprecated",
):
"""Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
@ -349,7 +370,6 @@ def stream(
headers=headers,
content_type=content_type,
status=status,
chunked=chunked,
)