From 3e4bec7f2cc3c9ed1ffd84ce51e77d6f0f3acb21 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Thu, 5 Nov 2020 14:53:48 +1000 Subject: [PATCH] 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. (cherry picked from commit c0839afddea13501a3098f8e27e686a6a95a71ee) --- sanic/app.py | 1 + sanic/asgi.py | 19 +++++++++++++++---- sanic/response.py | 2 ++ tests/test_response.py | 2 +- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index d12dc59b..e0fdbfe8 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1474,3 +1474,4 @@ class Sanic: self.asgi = True asgi_app = await ASGIApp.create(self, scope, receive, send) await asgi_app() + _asgi_single_callable = True # We conform to ASGI 3.0 single-callable diff --git a/sanic/asgi.py b/sanic/asgi.py index b5014b51..64cb6d44 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -309,13 +309,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( @@ -348,10 +352,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 4db94fc5..464eea39 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -80,6 +80,8 @@ class StreamingHTTPResponse(BaseHTTPResponse): if type(data) != bytes: 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 488a76e7..7438288d 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -235,7 +235,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):