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..f6bb27bf 100644 --- a/sanic/asgi.py +++ b/sanic/asgi.py @@ -312,13 +312,19 @@ class ASGIApp: callback = None if self.ws else self.stream_callback await handler(self.request, None, callback) - async def stream_callback(self, response: HTTPResponse) -> None: + _asgi_single_callable = True # We conform to ASGI 3.0 single-callable + + async def stream_callback( + self, response: Union[HTTPResponse, StreamingHTTPResponse] + ) -> 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,12 +357,22 @@ 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 + 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: headers += [ - (b"content-length", str(len(response.body)).encode("latin-1")) + (b"content-length", str(content_length[0]).encode("latin-1")) + ] + elif not is_streaming: + headers += [ + ( + b"content-length", + str(len(getattr(response, "body", b""))).encode("latin-1"), + ) ] if "content-type" not in response.headers: 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):