Everything but CustomServer OK.
This commit is contained in:
parent
85be5768c8
commit
2840e4cfc8
|
@ -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
|
||||
|
|
364
sanic/http.py
364
sanic/http.py
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue
Block a user