Everything but CustomServer OK.

This commit is contained in:
L. Kärkkäinen 2020-03-01 13:10:53 +02:00
parent 85be5768c8
commit 2840e4cfc8
4 changed files with 221 additions and 171 deletions

View File

@ -1028,6 +1028,8 @@ class Sanic:
response = await self._run_response_middleware(
request, response, request_name=name
)
except CancelledError:
raise
except Exception:
error_logger.exception(
"Exception occurred in one of response "
@ -1043,6 +1045,8 @@ class Sanic:
f"Invalid response type {response!r} (need HTTPResponse)"
)
except CancelledError:
raise
except Exception as e:
# -------------------------------------------- #
# Response Generation Failed

View File

@ -29,6 +29,24 @@ class Stage(Enum):
HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n"
class Http:
__slots__ = [
"_send",
"_receive_more",
"recv_buffer",
"protocol",
"expecting_continue",
"stage",
"keep_alive",
"head_only",
"request",
"exception",
"url",
"request_chunked",
"request_bytes_left",
"response",
"response_func",
"response_bytes_left",
]
def __init__(self, protocol):
self._send = protocol.send
self._receive_more = protocol.receive_more
@ -40,8 +58,46 @@ class Http:
self.head_only = None
self.request = None
self.exception = None
self.url = None
async def http1_receive_request(self):
async def http1(self):
"""HTTP 1.1 connection handler"""
while True: # As long as connection stays keep-alive
try:
# Receive and handle a request
self.stage = Stage.REQUEST
self.response_func = self.http1_response_start
await self.http1_request_header()
await self.protocol.request_handler(self.request)
# Handler finished, response should've been sent
if self.stage is Stage.HANDLER:
raise ServerError("Handler produced no response")
if self.stage is Stage.RESPONSE:
await self.send(end_stream=True)
# Consume any remaining request body (TODO: or disconnect?)
if self.request_bytes_left or self.request_chunked:
logger.error(f"{self.request} body not consumed.")
async for _ in self:
pass
except CancelledError:
# Write an appropriate response before exiting
e = self.exception or ServiceUnavailable(f"Cancelled")
self.exception = None
self.keep_alive = False
await self.error_response(e)
except Exception as e:
# Write an error response
await self.error_response(e)
# Exit and disconnect if finished
if self.stage is not Stage.IDLE or not self.keep_alive:
break
# Wait for next request
if not self.recv_buffer:
await self._receive_more()
async def http1_request_header(self):
"""Receive and parse request header into self.request."""
# Receive until full header is in buffer
buf = self.recv_buffer
pos = 0
while len(buf) < self.protocol.request_max_size:
@ -55,105 +111,195 @@ class Http:
self.stage = Stage.REQUEST
else:
raise PayloadTooLarge("Payload Too Large")
self.protocol._total_request_size = pos + 4
# Parse header content
try:
reqline, *headers = buf[:pos].decode().split("\r\n")
reqline, *raw_headers = buf[:pos].decode().split("\r\n")
method, self.url, protocol = reqline.split(" ")
if protocol not in ("HTTP/1.0", "HTTP/1.1"):
if protocol == "HTTP/1.1":
self.keep_alive = True
elif protocol == "HTTP/1.0":
self.keep_alive = False
else:
raise Exception
self.head_only = method.upper() == "HEAD"
headers = Header(
(name.lower(), value.lstrip())
for name, value in (h.split(":", 1) for h in headers)
)
body = False
headers = []
for name, value in (h.split(":", 1) for h in raw_headers):
name, value = h = name.lower(), value.lstrip()
if name in ("content-length", "transfer-encoding"):
body = True
elif name == "connection":
self.keep_alive = value.lower() == "keep-alive"
headers.append(h)
except:
raise InvalidUsage("Bad Request")
# Prepare a Request object
request = self.protocol.request_class(
url_bytes=self.url.encode(),
headers=headers,
headers=Header(headers),
version=protocol[-3:],
method=method,
transport=self.protocol.transport,
app=self.protocol.app,
)
# Prepare a request object from the header received
request.stream = self
self.protocol.state["requests_count"] += 1
self.keep_alive = (
protocol == "HTTP/1.1"
or headers.get("connection", "").lower() == "keep-alive"
)
# Prepare for request body
body = headers.get("transfer-encoding") == "chunked" or int(
headers.get("content-length", 0)
)
self.request_chunked = False
self.request_bytes_left = 0
if body:
headers = request.headers
expect = headers.get("expect")
if expect:
if expect is not None:
if expect.lower() == "100-continue":
self.expecting_continue = True
else:
raise HeaderExpectationFailed(
f"Unknown Expect: {expect}")
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
request.stream = self
if body is True:
if headers.get("transfer-encoding") == "chunked":
self.request_chunked = True
pos -= 2 # One CRLF stays in buffer
else:
self.request_bytes_left = body
self.request_bytes_left = int(headers["content-length"])
# Remove header and its trailing CRLF
del buf[: pos + 4]
self.stage = Stage.HANDLER
self.request = request
async def http1(self):
"""HTTP 1.1 connection handler"""
while self.stage is Stage.IDLE and self.keep_alive:
try:
# Receive request header and call handler
await self.http1_receive_request()
await self.protocol.request_handler(self.request)
if self.stage is Stage.HANDLER:
raise ServerError("Handler produced no response")
# Finish sending a response (if no error)
if self.stage is Stage.RESPONSE:
await self.send(end_stream=True)
# Consume any remaining request body
if self.request_bytes_left or self.request_chunked:
logger.error(f"{self.request} body not consumed.")
async for _ in self:
pass
except CancelledError:
# Exit after trying to finish a response
def http1_response_start(self, data, end_stream) -> bytes:
res = self.response
# Compatibility with simple response body
if not data and res.body:
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
# 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
if (
data
or not end_stream
or "content-length" in headers
or "transfer-encoding" in headers
):
# TODO: This matches old Sanic operation but possibly
# an exception would be more appropriate?
data, size, end_stream = b"", 0, True
headers.pop("content-length", None)
headers.pop("transfer-encoding", None)
#raise ServerError(
# f"A {status} response may only have headers, no body."
#)
elif self.head_only and "content-length" in headers:
self.response_func = None
elif end_stream:
# Non-streaming response (all in one block)
headers["content-length"] = size
self.response_func = None
elif "content-length" in headers:
# Streaming response with size known in advance
self.response_bytes_left = int(headers["content-length"]) - size
self.response_func = self.http1_response_normal
else:
# Length not known, use chunked encoding
headers["transfer-encoding"] = "chunked"
data = b"%x\r\n%b\r\n" % (size, data) if size else None
self.response_func = self.http1_response_chunked
if self.head_only:
# Head request: don't send body
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)
# Send a 100-continue if expected and not Expectation Failed
if self.expecting_continue:
self.expecting_continue = False
if status != 417:
ret = HTTP_CONTINUE + ret
# Send response
self.log_response()
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
return ret
def head_response_ignored(self, data, end_stream):
"""HEAD response: body data silently ignored."""
if end_stream:
self.response_func = None
self.stage = Stage.IDLE
def http1_response_chunked(self, data, end_stream) -> bytes:
"""Format a part of response body in chunked encoding."""
# Chunked encoding
size = len(data)
if end_stream:
self.response_func = None
self.stage = Stage.IDLE
if size:
return b"%x\r\n%b\r\n0\r\n\r\n" % (size, data)
return b"0\r\n\r\n"
return b"%x\r\n%b\r\n" % (size, data) if size else None
def http1_response_normal(self, data: bytes, end_stream: bool) -> bytes:
"""Format / keep track of non-chunked response."""
self.response_bytes_left -= len(data)
if self.response_bytes_left <= 0:
if self.response_bytes_left < 0:
raise ServerError("Response was bigger than content-length")
self.response_func = None
self.stage = Stage.IDLE
elif end_stream:
raise ServerError("Response was smaller than content-length")
return data
async def error_response(self, exception):
# Disconnect after an error if in any other state than handler
if self.stage is not Stage.HANDLER:
self.keep_alive = False
if self.exception is None:
self.exception = ServiceUnavailable(f"Cancelled")
except Exception as e:
self.exception = e
if self.exception:
e, self.exception = self.exception, None
# Exception while idle? Probably best to close connection
if self.stage is Stage.IDLE:
return
# Request failure? Try to respond but then disconnect
# Request failure? Respond but then disconnect
if self.stage is Stage.REQUEST:
self.keep_alive = False
self.stage = Stage.HANDLER
# Return an error page if possible
# From request and handler states we can respond, otherwise be silent
if self.stage is Stage.HANDLER:
app = self.protocol.app
response = await app.handle_exception(self.request, e)
response = await app.handle_exception(self.request, exception)
await self.respond(response).send(end_stream=True)
def log_response(self):
"""
Helper method provided to enable the logging of responses in case if
the :attr:`HttpProtocol.access_log` is enabled.
:param response: Response generated for the current request
:type response: :class:`sanic.response.HTTPResponse` or
:class:`sanic.response.StreamingHTTPResponse`
:return: None
"""
if self.protocol.access_log:
req, res = self.request, self.response
extra = {
"status": getattr(res, "status", 0),
"byte": getattr(self, "response_bytes_left", -1),
"host": "UNKNOWN",
"request": "nil",
}
if req is not None:
if req.ip:
extra["host"] = f"{req.ip}:{req.port}"
extra["request"] = f"{req.method} {req.url}"
access_logger.info("", extra=extra)
# Request methods
async def __aiter__(self):
"""Async iterate over request body."""
while True:
data = await self.read()
if not data:
@ -161,6 +307,7 @@ class Http:
yield data
async def read(self):
"""Read some bytes of request body."""
# Send a 100-continue if needed
if self.expecting_continue:
self.expecting_continue = False
@ -227,105 +374,8 @@ class Http:
"""
if data is None and end_stream is None:
end_stream = True
data = self.data_to_send(data, end_stream)
if data is None:
data = data.encode() if hasattr(data, "encode") else data or b""
data = self.response_func(data, end_stream)
if not data:
return
await self._send(data)
def data_to_send(self, data, end_stream):
"""Format output data bytes for given body data.
Headers are prepended to the first output block and then cleared.
:param data: str or bytes to be written
:return: bytes to send, or None if there is nothing to send
"""
data = data.encode() if hasattr(data, "encode") else data
size = len(data) if data is not None else 0
# Headers not yet sent?
if self.stage is Stage.HANDLER:
if self.response.body:
data = self.response.body + data if data else self.response.body
size = len(data)
r = self.response
status = r.status
headers = r.headers
if r.content_type and "content-type" not in headers:
headers["content-type"] = r.content_type
# Not Modified, Precondition Failed
if status in (304, 412):
headers = remove_entity_headers(headers)
if not has_message_body(status):
# Header-only response status
if (
size > 0
or not end_stream
or "content-length" in headers
or "transfer-encoding" in headers
):
# TODO: This matches old Sanic operation but possibly
# an exception would be more appropriate?
data = None
size = 0
end_stream = True
#raise ServerError(
# f"A {status} response may only have headers, no body."
#)
elif self.head_only and "content-length" in headers:
pass
elif end_stream:
# Non-streaming response (all in one block)
headers["content-length"] = size
elif "content-length" in headers:
# Streaming response with size known in advance
self.response_bytes_left = int(headers["content-length"]) - size
else:
# Length not known, use chunked encoding
headers["transfer-encoding"] = "chunked"
data = b"%x\r\n%b\r\n" % (size, data) if size else None
self.response_bytes_left = True
self.headers = None
if self.head_only:
data = None
self.response_bytes_left = None
if self.keep_alive:
headers["connection"] = "keep-alive"
headers["keep-alive"] = self.protocol.keep_alive_timeout
else:
headers["connection"] = "close"
ret = format_http1_response(status, headers.items(), data or b"")
# Send a 100-continue if expected and not Expectation Failed
if self.expecting_continue:
self.expecting_continue = False
if status != 417:
ret = HTTP_CONTINUE + ret
# Send response
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
return ret
# HEAD request: don't send body
if self.head_only:
return None
if self.stage is not Stage.RESPONSE:
if size:
raise RuntimeError("Cannot send data to a closed stream")
return
# Chunked encoding
if self.response_bytes_left is True:
if end_stream:
self.response_bytes_left = None
self.stage = Stage.IDLE
if size:
return b"%x\r\n%b\r\n0\r\n\r\n" % (size, data)
return b"0\r\n\r\n"
return b"%x\r\n%b\r\n" % (size, data) if size else None
# Normal encoding
else:
self.response_bytes_left -= size
if self.response_bytes_left <= 0:
if self.response_bytes_left < 0:
raise ServerError("Response was bigger than content-length")
self.stage = Stage.IDLE
return data if size else None

View File

@ -26,7 +26,7 @@ from sanic.exceptions import (
ServiceUnavailable,
)
from sanic.http import Http, Stage
from sanic.log import access_logger, logger
from sanic.log import logger
from sanic.request import Request
@ -181,7 +181,7 @@ class HttpProtocol(asyncio.Protocol):
stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED)
and duration > self.response_timeout
):
self._http.exception = RequestTimeout("Response Timeout")
self._http.exception = ServiceUnavailable("Response Timeout")
else:
interval = min(self.keep_alive_timeout, self.request_timeout, self.response_timeout) / 2
self.loop.call_later(max(0.1, interval), self.check_timeouts)

View File

@ -79,12 +79,8 @@ class DelayableSanicTestClient(SanicTestClient):
request_timeout_default_app = Sanic("test_request_timeout_default")
request_no_timeout_app = Sanic("test_request_no_timeout")
# Note: The delayed client pauses before making a request, so technically
# it is in keep alive duration. Earlier Sanic versions entered a new connection
# in request mode even if no bytes of request were received.
request_timeout_default_app.config.KEEP_ALIVE_TIMEOUT = 0.6
request_no_timeout_app.config.KEEP_ALIVE_TIMEOUT = 0.6
request_timeout_default_app.config.REQUEST_TIMEOUT = 0.6
request_no_timeout_app.config.REQUEST_TIMEOUT = 0.6
@request_timeout_default_app.route("/1")