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 yield data
def respond(self, response): def respond(self, response):
headers: List[Tuple[bytes, bytes]] = [] response.stream, self.response = self, response
cookies: Dict[str, str] = {} return response
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",)
]
if "content-length" not in response.headers and not isinstance( async def send(self, data, end_stream):
response, StreamingHTTPResponse if self.response:
): response, self.response = self.response, None
headers += [ await self.transport.send({
(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", "type": "http.response.start",
"status": response.status, "status": response.status,
"headers": headers, "headers": response.full_headers,
} })
self.response_body = response.body response_body = getattr(response, "body", None)
return self if response_body:
data = response_body + data if data else response_body
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
await self.transport.send( await self.transport.send(
{ {
"type": "http.response.body", "type": "http.response.body",

View File

@ -77,7 +77,7 @@ class Http:
if self.stage is Stage.HANDLER: if self.stage is Stage.HANDLER:
raise ServerError("Handler produced no response") raise ServerError("Handler produced no response")
if self.stage is Stage.RESPONSE: if self.stage is Stage.RESPONSE:
await self.send(end_stream=True) await self.response.send(end_stream=True)
except CancelledError: except CancelledError:
# Write an appropriate response before exiting # Write an appropriate response before exiting
e = self.exception or ServiceUnavailable(f"Cancelled") e = self.exception or ServiceUnavailable(f"Cancelled")
@ -178,13 +178,15 @@ class Http:
def http1_response_header(self, data, end_stream) -> bytes: def http1_response_header(self, data, end_stream) -> bytes:
res = self.response res = self.response
# Compatibility with simple response body # 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 data, end_stream = res.body, True
size = len(data) size = len(data)
status = res.status
headers = res.headers headers = res.headers
if res.content_type and "content-type" not in headers: if res.content_type and "content-type" not in headers:
headers["content-type"] = res.content_type 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 # Not Modified, Precondition Failed
if status in (304, 412): if status in (304, 412):
headers = remove_entity_headers(headers) headers = remove_entity_headers(headers)
@ -374,19 +376,10 @@ class Http:
if self.stage is not Stage.HANDLER: if self.stage is not Stage.HANDLER:
self.stage = Stage.FAILED self.stage = Stage.FAILED
raise RuntimeError("Response already started") raise RuntimeError("Response already started")
if not isinstance(response.status, int) or response.status < 200: self.response, response.stream = response, self
raise RuntimeError(f"Invalid response status {response.status!r}") return response
self.response = response
return self
async def send(self, data=None, end_stream=None): async def send(self, data, end_stream):
"""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""
data = self.response_func(data, end_stream) data = self.response_func(data, end_stream)
if not data: if not data:
return return

View File

@ -106,15 +106,17 @@ class Request:
) )
def respond( 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( # This logic of determining which response to use is subject to change
status if response is None:
if isinstance(status, HTTPResponse) response = self.stream.response or HTTPResponse(
else HTTPResponse( status=status,
status=status, headers=headers, content_type=content_type, headers=headers,
) content_type=content_type,
) )
# Connect the response and return it
return self.stream.respond(response)
async def receive_body(self): async def receive_body(self):
self.body = b"".join([data async for data in self.stream]) 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 return data.encode() if hasattr(data, "encode") else data
def _parse_headers(self): def _parse_headers(self):
return format_http1(self.headers.items()) return format_http1(self.full_headers)
@property @property
def cookies(self): def cookies(self):
@ -34,6 +34,43 @@ class BaseHTTPResponse:
self._cookies = CookieJar(self.headers) self._cookies = CookieJar(self.headers)
return self._cookies 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): class StreamingHTTPResponse(BaseHTTPResponse):
__slots__ = ( __slots__ = (
@ -42,9 +79,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
"status", "status",
"content_type", "content_type",
"headers", "headers",
"chunked",
"_cookies", "_cookies",
"send",
) )
def __init__( def __init__(
@ -53,13 +88,12 @@ class StreamingHTTPResponse(BaseHTTPResponse):
status=200, status=200,
headers=None, headers=None,
content_type="text/plain; charset=utf-8", content_type="text/plain; charset=utf-8",
chunked=True, chunked="deprecated",
): ):
self.content_type = content_type self.content_type = content_type
self.streaming_fn = streaming_fn self.streaming_fn = streaming_fn
self.status = status self.status = status
self.headers = Header(headers or {}) self.headers = Header(headers or {})
self.chunked = chunked
self._cookies = None self._cookies = None
async def write(self, data): async def write(self, data):
@ -70,9 +104,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
await self.send(self._encode_body(data)) await self.send(self._encode_body(data))
async def stream(self, request): async def stream(self, request):
self.send = request.respond( request.respond(self)
self.status, self.headers, self.content_type,
).send
await self.streaming_fn(self) await self.streaming_fn(self)
await self.send(end_stream=True) await self.send(end_stream=True)
@ -94,16 +126,6 @@ class HTTPResponse(BaseHTTPResponse):
self.headers = Header(headers or {}) self.headers = Header(headers or {})
self._cookies = None 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 @property
def cookies(self): def cookies(self):
if self._cookies is None: if self._cookies is None:
@ -265,7 +287,7 @@ async def file_stream(
mime_type=None, mime_type=None,
headers=None, headers=None,
filename=None, filename=None,
chunked=True, chunked="deprecated",
_range=None, _range=None,
): ):
"""Return a streaming response object with file data. """Return a streaming response object with file data.
@ -314,7 +336,6 @@ async def file_stream(
status=status, status=status,
headers=headers, headers=headers,
content_type=mime_type, content_type=mime_type,
chunked=chunked,
) )
@ -323,7 +344,7 @@ def stream(
status=200, status=200,
headers=None, headers=None,
content_type="text/plain; charset=utf-8", content_type="text/plain; charset=utf-8",
chunked=True, chunked="deprecated",
): ):
"""Accepts an coroutine `streaming_fn` which can be used to """Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`. write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
@ -349,7 +370,6 @@ def stream(
headers=headers, headers=headers,
content_type=content_type, content_type=content_type,
status=status, status=status,
chunked=chunked,
) )