Compare commits

..

6 Commits

Author SHA1 Message Date
Adam Hopkins
5a48b94089 Bump version 21.6.1 2021-07-28 11:41:26 +03:00
L. Kärkkäinen
ba1c73d947 Fix issues with after request handling in HTTP pipelining (#2201)
* Clean up after a request is complete, before the next pipelined request.

* Limit the size of request body consumed after handler has finished.

* Linter error.

* Add unit test re: bad headers

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2021-07-28 11:40:34 +03:00
Adam Hopkins
a6e78b70ab Resolve regressions in exceptions (#2181) 2021-07-28 11:37:24 +03:00
L. Kärkkäinen
bb1174afc5 Fix the handling of the end of a chunked request. (#2188)
* Fix the handling of the end of a chunked request.

* Avoid hardcoding final chunk header size.

* Add some unit tests for pipeline body reading

* Decode bytes for json serialization

Co-authored-by: L. Kärkkäinen <tronic@users.noreply.github.com>
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2021-07-28 11:36:56 +03:00
Adam Hopkins
df8abe9cfd Manually reset the buffer when streaming request body (#2183) 2021-07-28 11:34:57 +03:00
Robert Palmer
c3bca97ee1 Update sanic-routing to fix path issues plus lookahead / lookbehind support (#2178)
* Update sanic-routing to fix path issues plus lookahead / lookbehind support

* Update setup.py

Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-07-28 11:33:53 +03:00
6 changed files with 153 additions and 14 deletions

View File

@@ -1 +1 @@
__version__ = "21.6.0"
__version__ = "21.6.1"

View File

@@ -31,6 +31,7 @@ class NotFound(SanicException):
"""
status_code = 404
quiet = True
class InvalidUsage(SanicException):
@@ -39,6 +40,7 @@ class InvalidUsage(SanicException):
"""
status_code = 400
quiet = True
class MethodNotSupported(SanicException):
@@ -47,6 +49,7 @@ class MethodNotSupported(SanicException):
"""
status_code = 405
quiet = True
def __init__(self, message, method, allowed_methods):
super().__init__(message)
@@ -70,6 +73,7 @@ class ServiceUnavailable(SanicException):
"""
status_code = 503
quiet = True
class URLBuildError(ServerError):
@@ -101,6 +105,7 @@ class RequestTimeout(SanicException):
"""
status_code = 408
quiet = True
class PayloadTooLarge(SanicException):
@@ -109,6 +114,7 @@ class PayloadTooLarge(SanicException):
"""
status_code = 413
quiet = True
class HeaderNotFound(InvalidUsage):
@@ -117,6 +123,7 @@ class HeaderNotFound(InvalidUsage):
"""
status_code = 400
quiet = True
class ContentRangeError(SanicException):
@@ -125,6 +132,7 @@ class ContentRangeError(SanicException):
"""
status_code = 416
quiet = True
def __init__(self, message, content_range):
super().__init__(message)
@@ -137,6 +145,7 @@ class HeaderExpectationFailed(SanicException):
"""
status_code = 417
quiet = True
class Forbidden(SanicException):
@@ -145,6 +154,7 @@ class Forbidden(SanicException):
"""
status_code = 403
quiet = True
class InvalidRangeType(ContentRangeError):
@@ -153,6 +163,7 @@ class InvalidRangeType(ContentRangeError):
"""
status_code = 416
quiet = True
class PyFileError(Exception):
@@ -196,6 +207,7 @@ class Unauthorized(SanicException):
"""
status_code = 401
quiet = True
def __init__(self, message, status_code=None, scheme=None, **kwargs):
super().__init__(message, status_code)

View File

@@ -95,19 +95,23 @@ class Http:
self._receive_more = protocol.receive_more
self.recv_buffer = protocol.recv_buffer
self.protocol = protocol
self.expecting_continue: bool = False
self.keep_alive = True
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_bytes = None
self.request_bytes_left = None
self.request_max_size = protocol.request_max_size
self.keep_alive = True
self.head_only = None
self.request_max_size = self.protocol.request_max_size
self.request: Request = None
self.response: BaseHTTPResponse = None
self.exception = None
self.url = None
self.upgrade_websocket = False
self.url = None
def __bool__(self):
"""Test if request handling is in progress"""
@@ -148,7 +152,10 @@ class Http:
if self.request_body:
if self.response and 200 <= self.response.status < 300:
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:
async for _ in self:
pass
@@ -160,11 +167,19 @@ class Http:
await sleep(0.001)
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
if self.stage is not Stage.IDLE or not self.keep_alive:
break
# Wait for next request
# Wait for the next request
if not self.recv_buffer:
await self._receive_more()
@@ -486,8 +501,6 @@ class Http:
self.keep_alive = False
raise InvalidUsage("Bad chunked encoding")
del buf[: pos + 2]
if size <= 0:
self.request_body = None
@@ -495,8 +508,17 @@ class Http:
self.keep_alive = False
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
# Remove CRLF, chunk size and the CRLF that follows
del buf[: pos + 2]
self.request_bytes_left = size
self.request_bytes += size

View File

@@ -83,7 +83,7 @@ ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency
requirements = [
"sanic-routing==0.7.0",
"sanic-routing~=0.7",
"httptools>=0.0.10",
uvloop,
ujson,
@@ -93,7 +93,7 @@ requirements = [
]
tests_require = [
"sanic-testing>=0.6.0",
"sanic-testing>=0.7.0b1",
"pytest==5.2.1",
"multidict>=5.0,<6.0",
"gunicorn==20.0.4",

105
tests/test_pipelining.py Normal file
View 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"]
)

View File

@@ -471,7 +471,7 @@ def test_stack_trace_on_not_found(app, static_file_directory, caplog):
assert response.status == 404
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):