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>
This commit is contained in:
L. Kärkkäinen 2021-07-11 06:44:40 -04:00 committed by GitHub
parent 1dd0332e8b
commit 08a4b3013f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 92 additions and 6 deletions

View File

@ -486,20 +486,24 @@ 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
# Because we are leaving one CRLF in the buffer, we manually
# reset the buffer here
self.recv_buffer = bytearray()
if size < 0: if size < 0:
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

View File

@ -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",
"coverage==5.3", "coverage==5.3",
"gunicorn==20.0.4", "gunicorn==20.0.4",

82
tests/test_pipelining.py Normal file
View File

@ -0,0 +1,82 @@
from httpx import AsyncByteStream
from sanic_testing.reusable import ReusableClient
from sanic.response import json
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"]