diff --git a/sanic/http.py b/sanic/http.py index a594e60c..16603c38 100644 --- a/sanic/http.py +++ b/sanic/http.py @@ -486,20 +486,24 @@ class Http: self.keep_alive = False raise InvalidUsage("Bad chunked encoding") - del buf[: pos + 2] - if size <= 0: 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: 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 diff --git a/setup.py b/setup.py index 3bdd68b0..fe766ff7 100644 --- a/setup.py +++ b/setup.py @@ -93,7 +93,7 @@ requirements = [ ] tests_require = [ - "sanic-testing>=0.6.0", + "sanic-testing>=0.7.0b1", "pytest==5.2.1", "coverage==5.3", "gunicorn==20.0.4", diff --git a/tests/test_pipelining.py b/tests/test_pipelining.py new file mode 100644 index 00000000..689a787b --- /dev/null +++ b/tests/test_pipelining.py @@ -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"]