From c0839afddea13501a3098f8e27e686a6a95a71ee Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Thu, 5 Nov 2020 14:53:48 +1000 Subject: [PATCH 1/2] Fix Chunked Transport-Encoding in ASGI streaming response In ASGI-mode, don't do sanic-side response chunk encoding, leave that to the ASGI-response-transport Don't set content-length when using chunked-encoding in ASGI mode, this is incompatible with ASGI Chunked Transport-Encoding. --- sanic/app.py | 2 ++ sanic/asgi.py | 19 +++++++++++++++---- sanic/response.py | 2 ++ tests/test_response.py | 2 +- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 3f48f1fd..e8cb5c17 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1454,6 +1454,8 @@ class Sanic: asgi_app = await ASGIApp.create(self, scope, receive, send) await asgi_app() + _asgi_single_callable = True # We conform to ASGI 3.0 single-callable + # -------------------------------------------------------------------- # # Configuration # -------------------------------------------------------------------- # diff --git a/sanic/asgi.py b/sanic/asgi.py index 2a3c4540..8e13342c 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -312,13 +312,17 @@ class ASGIApp: callback = None if self.ws else self.stream_callback await handler(self.request, None, callback) + _asgi_single_callable = True # We conform to ASGI 3.0 single-callable + async def stream_callback(self, response: HTTPResponse) -> None: """ Write the response. """ headers: List[Tuple[bytes, bytes]] = [] cookies: Dict[str, str] = {} + content_length: List[str] = [] try: + content_length = response.headers.popall("content-length", []) cookies = { v.key: v for _, v in list( @@ -351,10 +355,17 @@ class ASGIApp: ] response.asgi = True - - if "content-length" not in response.headers and not isinstance( - response, StreamingHTTPResponse - ): + is_streaming = isinstance(response, StreamingHTTPResponse) + if is_streaming and getattr(response, "chunked", False): + # disable sanic chunking, this is done at the ASGI-server level + response.chunked = False + # content-length header is removed to signal to the ASGI-server + # to use automatic-chunking if it supports it + elif len(content_length) > 0: + headers += [ + (b"content-length", str(content_length[0]).encode("latin-1")) + ] + elif not is_streaming: headers += [ (b"content-length", str(len(response.body)).encode("latin-1")) ] diff --git a/sanic/response.py b/sanic/response.py index 1f7b12de..9841bb2d 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -100,6 +100,8 @@ class StreamingHTTPResponse(BaseHTTPResponse): """ data = self._encode_body(data) + # `chunked` will always be False in ASGI-mode, even if the underlying + # ASGI Transport implements Chunked transport. That does it itself. if self.chunked: await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) else: diff --git a/tests/test_response.py b/tests/test_response.py index 6e2f2a9a..847246d3 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -238,7 +238,7 @@ def test_chunked_streaming_returns_correct_content(streaming_app): @pytest.mark.asyncio async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): request, response = await streaming_app.asgi_client.get("/") - assert response.text == "4\r\nfoo,\r\n3\r\nbar\r\n0\r\n\r\n" + assert response.text == "foo,bar" def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): From 75994cd915973c161fd9cb0f3f8c140d2f4a003e Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Thu, 5 Nov 2020 08:49:55 +0200 Subject: [PATCH 2/2] Fixes for linting and type hints --- sanic/asgi.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sanic/asgi.py b/sanic/asgi.py index 8e13342c..f6bb27bf 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -314,7 +314,9 @@ class ASGIApp: _asgi_single_callable = True # We conform to ASGI 3.0 single-callable - async def stream_callback(self, response: HTTPResponse) -> None: + async def stream_callback( + self, response: Union[HTTPResponse, StreamingHTTPResponse] + ) -> None: """ Write the response. """ @@ -358,7 +360,7 @@ class ASGIApp: is_streaming = isinstance(response, StreamingHTTPResponse) if is_streaming and getattr(response, "chunked", False): # disable sanic chunking, this is done at the ASGI-server level - response.chunked = False + setattr(response, "chunked", False) # content-length header is removed to signal to the ASGI-server # to use automatic-chunking if it supports it elif len(content_length) > 0: @@ -367,7 +369,10 @@ class ASGIApp: ] elif not is_streaming: headers += [ - (b"content-length", str(len(response.body)).encode("latin-1")) + ( + b"content-length", + str(len(getattr(response, "body", b""))).encode("latin-1"), + ) ] if "content-type" not in response.headers: