Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the handling of the end of a chunked request. #2188

Merged
merged 4 commits into from
Jul 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions sanic/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,20 +486,24 @@ async def read(self) -> Optional[bytes]:
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

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def open_local(paths, mode="r", encoding="utf8"):
]

tests_require = [
"sanic-testing>=0.6.0",
"sanic-testing>=0.7.0b1",
"pytest==5.2.1",
"coverage==5.3",
"gunicorn==20.0.4",
Expand Down
82 changes: 82 additions & 0 deletions tests/test_pipelining.py
Original file line number Diff line number Diff line change
@@ -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"]