Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a48b94089 | ||
|
|
ba1c73d947 | ||
|
|
a6e78b70ab | ||
|
|
bb1174afc5 | ||
|
|
df8abe9cfd | ||
|
|
c3bca97ee1 |
@@ -1 +1 @@
|
|||||||
__version__ = "21.6.0"
|
__version__ = "21.6.1"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class NotFound(SanicException):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 404
|
status_code = 404
|
||||||
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class InvalidUsage(SanicException):
|
class InvalidUsage(SanicException):
|
||||||
@@ -39,6 +40,7 @@ class InvalidUsage(SanicException):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 400
|
status_code = 400
|
||||||
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class MethodNotSupported(SanicException):
|
class MethodNotSupported(SanicException):
|
||||||
@@ -47,6 +49,7 @@ class MethodNotSupported(SanicException):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 405
|
status_code = 405
|
||||||
|
quiet = True
|
||||||
|
|
||||||
def __init__(self, message, method, allowed_methods):
|
def __init__(self, message, method, allowed_methods):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
@@ -70,6 +73,7 @@ class ServiceUnavailable(SanicException):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 503
|
status_code = 503
|
||||||
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class URLBuildError(ServerError):
|
class URLBuildError(ServerError):
|
||||||
@@ -101,6 +105,7 @@ class RequestTimeout(SanicException):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 408
|
status_code = 408
|
||||||
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class PayloadTooLarge(SanicException):
|
class PayloadTooLarge(SanicException):
|
||||||
@@ -109,6 +114,7 @@ class PayloadTooLarge(SanicException):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 413
|
status_code = 413
|
||||||
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class HeaderNotFound(InvalidUsage):
|
class HeaderNotFound(InvalidUsage):
|
||||||
@@ -117,6 +123,7 @@ class HeaderNotFound(InvalidUsage):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 400
|
status_code = 400
|
||||||
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class ContentRangeError(SanicException):
|
class ContentRangeError(SanicException):
|
||||||
@@ -125,6 +132,7 @@ class ContentRangeError(SanicException):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 416
|
status_code = 416
|
||||||
|
quiet = True
|
||||||
|
|
||||||
def __init__(self, message, content_range):
|
def __init__(self, message, content_range):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
@@ -137,6 +145,7 @@ class HeaderExpectationFailed(SanicException):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 417
|
status_code = 417
|
||||||
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class Forbidden(SanicException):
|
class Forbidden(SanicException):
|
||||||
@@ -145,6 +154,7 @@ class Forbidden(SanicException):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 403
|
status_code = 403
|
||||||
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class InvalidRangeType(ContentRangeError):
|
class InvalidRangeType(ContentRangeError):
|
||||||
@@ -153,6 +163,7 @@ class InvalidRangeType(ContentRangeError):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 416
|
status_code = 416
|
||||||
|
quiet = True
|
||||||
|
|
||||||
|
|
||||||
class PyFileError(Exception):
|
class PyFileError(Exception):
|
||||||
@@ -196,6 +207,7 @@ class Unauthorized(SanicException):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
status_code = 401
|
status_code = 401
|
||||||
|
quiet = True
|
||||||
|
|
||||||
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
||||||
super().__init__(message, status_code)
|
super().__init__(message, status_code)
|
||||||
|
|||||||
@@ -95,19 +95,23 @@ class Http:
|
|||||||
self._receive_more = protocol.receive_more
|
self._receive_more = protocol.receive_more
|
||||||
self.recv_buffer = protocol.recv_buffer
|
self.recv_buffer = protocol.recv_buffer
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
self.expecting_continue: bool = False
|
self.keep_alive = True
|
||||||
self.stage: Stage = Stage.IDLE
|
self.stage: Stage = Stage.IDLE
|
||||||
|
self.init_for_request()
|
||||||
|
|
||||||
|
def init_for_request(self):
|
||||||
|
"""Init/reset all per-request variables."""
|
||||||
|
self.exception = None
|
||||||
|
self.expecting_continue: bool = False
|
||||||
|
self.head_only = None
|
||||||
self.request_body = None
|
self.request_body = None
|
||||||
self.request_bytes = None
|
self.request_bytes = None
|
||||||
self.request_bytes_left = None
|
self.request_bytes_left = None
|
||||||
self.request_max_size = protocol.request_max_size
|
self.request_max_size = self.protocol.request_max_size
|
||||||
self.keep_alive = True
|
|
||||||
self.head_only = None
|
|
||||||
self.request: Request = None
|
self.request: Request = None
|
||||||
self.response: BaseHTTPResponse = None
|
self.response: BaseHTTPResponse = None
|
||||||
self.exception = None
|
|
||||||
self.url = None
|
|
||||||
self.upgrade_websocket = False
|
self.upgrade_websocket = False
|
||||||
|
self.url = None
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
"""Test if request handling is in progress"""
|
"""Test if request handling is in progress"""
|
||||||
@@ -148,7 +152,10 @@ class Http:
|
|||||||
if self.request_body:
|
if self.request_body:
|
||||||
if self.response and 200 <= self.response.status < 300:
|
if self.response and 200 <= self.response.status < 300:
|
||||||
error_logger.error(f"{self.request} body not consumed.")
|
error_logger.error(f"{self.request} body not consumed.")
|
||||||
|
# Limit the size because the handler may have set it infinite
|
||||||
|
self.request_max_size = min(
|
||||||
|
self.request_max_size, self.protocol.request_max_size
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
async for _ in self:
|
async for _ in self:
|
||||||
pass
|
pass
|
||||||
@@ -160,11 +167,19 @@ class Http:
|
|||||||
await sleep(0.001)
|
await sleep(0.001)
|
||||||
self.keep_alive = False
|
self.keep_alive = False
|
||||||
|
|
||||||
|
# Clean up to free memory and for the next request
|
||||||
|
if self.request:
|
||||||
|
self.request.stream = None
|
||||||
|
if self.response:
|
||||||
|
self.response.stream = None
|
||||||
|
|
||||||
|
self.init_for_request()
|
||||||
|
|
||||||
# Exit and disconnect if no more requests can be taken
|
# Exit and disconnect if no more requests can be taken
|
||||||
if self.stage is not Stage.IDLE or not self.keep_alive:
|
if self.stage is not Stage.IDLE or not self.keep_alive:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Wait for next request
|
# Wait for the next request
|
||||||
if not self.recv_buffer:
|
if not self.recv_buffer:
|
||||||
await self._receive_more()
|
await self._receive_more()
|
||||||
|
|
||||||
@@ -486,8 +501,6 @@ class Http:
|
|||||||
self.keep_alive = False
|
self.keep_alive = False
|
||||||
raise InvalidUsage("Bad chunked encoding")
|
raise InvalidUsage("Bad chunked encoding")
|
||||||
|
|
||||||
del buf[: pos + 2]
|
|
||||||
|
|
||||||
if size <= 0:
|
if size <= 0:
|
||||||
self.request_body = None
|
self.request_body = None
|
||||||
|
|
||||||
@@ -495,8 +508,17 @@ class Http:
|
|||||||
self.keep_alive = False
|
self.keep_alive = False
|
||||||
raise InvalidUsage("Bad chunked encoding")
|
raise InvalidUsage("Bad chunked encoding")
|
||||||
|
|
||||||
|
# Consume CRLF, chunk size 0 and the two CRLF that follow
|
||||||
|
pos += 4
|
||||||
|
# Might need to wait for the final CRLF
|
||||||
|
while len(buf) < pos:
|
||||||
|
await self._receive_more()
|
||||||
|
del buf[:pos]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Remove CRLF, chunk size and the CRLF that follows
|
||||||
|
del buf[: pos + 2]
|
||||||
|
|
||||||
self.request_bytes_left = size
|
self.request_bytes_left = size
|
||||||
self.request_bytes += size
|
self.request_bytes += size
|
||||||
|
|
||||||
|
|||||||
4
setup.py
4
setup.py
@@ -83,7 +83,7 @@ ujson = "ujson>=1.35" + env_dependency
|
|||||||
uvloop = "uvloop>=0.5.3" + env_dependency
|
uvloop = "uvloop>=0.5.3" + env_dependency
|
||||||
|
|
||||||
requirements = [
|
requirements = [
|
||||||
"sanic-routing==0.7.0",
|
"sanic-routing~=0.7",
|
||||||
"httptools>=0.0.10",
|
"httptools>=0.0.10",
|
||||||
uvloop,
|
uvloop,
|
||||||
ujson,
|
ujson,
|
||||||
@@ -93,7 +93,7 @@ requirements = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
"sanic-testing>=0.6.0",
|
"sanic-testing>=0.7.0b1",
|
||||||
"pytest==5.2.1",
|
"pytest==5.2.1",
|
||||||
"multidict>=5.0,<6.0",
|
"multidict>=5.0,<6.0",
|
||||||
"gunicorn==20.0.4",
|
"gunicorn==20.0.4",
|
||||||
|
|||||||
105
tests/test_pipelining.py
Normal file
105
tests/test_pipelining.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
from httpx import AsyncByteStream
|
||||||
|
from sanic_testing.reusable import ReusableClient
|
||||||
|
|
||||||
|
from sanic.response import json, text
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_body_requests(app):
|
||||||
|
@app.get("/")
|
||||||
|
async def handler(request):
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
"request_id": str(request.id),
|
||||||
|
"connection_id": id(request.conn_info),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
client = ReusableClient(app, port=1234)
|
||||||
|
|
||||||
|
with client:
|
||||||
|
_, response1 = client.get("/")
|
||||||
|
_, response2 = client.get("/")
|
||||||
|
|
||||||
|
assert response1.status == response2.status == 200
|
||||||
|
assert response1.json["request_id"] != response2.json["request_id"]
|
||||||
|
assert response1.json["connection_id"] == response2.json["connection_id"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_body_requests(app):
|
||||||
|
@app.post("/")
|
||||||
|
async def handler(request):
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
"request_id": str(request.id),
|
||||||
|
"connection_id": id(request.conn_info),
|
||||||
|
"foo": request.json.get("foo"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
client = ReusableClient(app, port=1234)
|
||||||
|
|
||||||
|
with client:
|
||||||
|
_, response1 = client.post("/", json={"foo": True})
|
||||||
|
_, response2 = client.post("/", json={"foo": True})
|
||||||
|
|
||||||
|
assert response1.status == response2.status == 200
|
||||||
|
assert response1.json["foo"] is response2.json["foo"] is True
|
||||||
|
assert response1.json["request_id"] != response2.json["request_id"]
|
||||||
|
assert response1.json["connection_id"] == response2.json["connection_id"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_streaming_body_requests(app):
|
||||||
|
@app.post("/", stream=True)
|
||||||
|
async def handler(request):
|
||||||
|
data = [part.decode("utf-8") async for part in request.stream]
|
||||||
|
return json(
|
||||||
|
{
|
||||||
|
"request_id": str(request.id),
|
||||||
|
"connection_id": id(request.conn_info),
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
data = ["hello", "world"]
|
||||||
|
|
||||||
|
class Data(AsyncByteStream):
|
||||||
|
def __init__(self, data):
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
async def __aiter__(self):
|
||||||
|
for value in self.data:
|
||||||
|
yield value.encode("utf-8")
|
||||||
|
|
||||||
|
client = ReusableClient(app, port=1234)
|
||||||
|
|
||||||
|
with client:
|
||||||
|
_, response1 = client.post("/", data=Data(data))
|
||||||
|
_, response2 = client.post("/", data=Data(data))
|
||||||
|
|
||||||
|
assert response1.status == response2.status == 200
|
||||||
|
assert response1.json["data"] == response2.json["data"] == data
|
||||||
|
assert response1.json["request_id"] != response2.json["request_id"]
|
||||||
|
assert response1.json["connection_id"] == response2.json["connection_id"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_headers(app):
|
||||||
|
@app.get("/")
|
||||||
|
async def handler(request):
|
||||||
|
return text("")
|
||||||
|
|
||||||
|
@app.on_response
|
||||||
|
async def reqid(request, response):
|
||||||
|
response.headers["x-request-id"] = request.id
|
||||||
|
|
||||||
|
client = ReusableClient(app, port=1234)
|
||||||
|
bad_headers = {"bad": "bad" * 5_000}
|
||||||
|
|
||||||
|
with client:
|
||||||
|
_, response1 = client.get("/")
|
||||||
|
_, response2 = client.get("/", headers=bad_headers)
|
||||||
|
|
||||||
|
assert response1.status == 200
|
||||||
|
assert response2.status == 413
|
||||||
|
assert (
|
||||||
|
response1.headers["x-request-id"] != response2.headers["x-request-id"]
|
||||||
|
)
|
||||||
@@ -471,7 +471,7 @@ def test_stack_trace_on_not_found(app, static_file_directory, caplog):
|
|||||||
|
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
assert counter[logging.INFO] == 5
|
assert counter[logging.INFO] == 5
|
||||||
assert counter[logging.ERROR] == 1
|
assert counter[logging.ERROR] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
|
def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
|
||||||
|
|||||||
Reference in New Issue
Block a user