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:
		| @@ -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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								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", | ||||
|   | ||||
							
								
								
									
										82
									
								
								tests/test_pipelining.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								tests/test_pipelining.py
									
									
									
									
									
										Normal 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"] | ||||
		Reference in New Issue
	
	Block a user
	 L. Kärkkäinen
					L. Kärkkäinen