Compare commits
	
		
			5 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 9b24fbb2f3 | ||
|   | 468f4ac7f1 | ||
|   | be1ca93a23 | ||
|   | 662c7c7f62 | ||
|   | 3e4bec7f2c | 
| @@ -1 +1 @@ | |||||||
| __version__ = "19.12.3" | __version__ = "19.12.4" | ||||||
|   | |||||||
| @@ -1474,3 +1474,5 @@ class Sanic: | |||||||
|         self.asgi = True |         self.asgi = True | ||||||
|         asgi_app = await ASGIApp.create(self, scope, receive, send) |         asgi_app = await ASGIApp.create(self, scope, receive, send) | ||||||
|         await asgi_app() |         await asgi_app() | ||||||
|  |  | ||||||
|  |     _asgi_single_callable = True  # We conform to ASGI 3.0 single-callable | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import asyncio | import asyncio | ||||||
| import warnings | import warnings | ||||||
|  |  | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from typing import ( | from typing import ( | ||||||
|     Any, |     Any, | ||||||
| @@ -15,6 +16,7 @@ from typing import ( | |||||||
| from urllib.parse import quote | from urllib.parse import quote | ||||||
|  |  | ||||||
| import sanic.app  # noqa | import sanic.app  # noqa | ||||||
|  |  | ||||||
| from sanic.compat import Header | from sanic.compat import Header | ||||||
| from sanic.exceptions import InvalidUsage, ServerError | from sanic.exceptions import InvalidUsage, ServerError | ||||||
| from sanic.log import logger | from sanic.log import logger | ||||||
| @@ -23,6 +25,7 @@ from sanic.response import HTTPResponse, StreamingHTTPResponse | |||||||
| from sanic.server import StreamBuffer | from sanic.server import StreamBuffer | ||||||
| from sanic.websocket import WebSocketConnection | from sanic.websocket import WebSocketConnection | ||||||
|  |  | ||||||
|  |  | ||||||
| ASGIScope = MutableMapping[str, Any] | ASGIScope = MutableMapping[str, Any] | ||||||
| ASGIMessage = MutableMapping[str, Any] | ASGIMessage = MutableMapping[str, Any] | ||||||
| ASGISend = Callable[[ASGIMessage], Awaitable[None]] | ASGISend = Callable[[ASGIMessage], Awaitable[None]] | ||||||
| @@ -65,7 +68,9 @@ class MockProtocol: | |||||||
| class MockTransport: | class MockTransport: | ||||||
|     _protocol: Optional[MockProtocol] |     _protocol: Optional[MockProtocol] | ||||||
|  |  | ||||||
|     def __init__(self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend) -> None: |     def __init__( | ||||||
|  |         self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend | ||||||
|  |     ) -> None: | ||||||
|         self.scope = scope |         self.scope = scope | ||||||
|         self._receive = receive |         self._receive = receive | ||||||
|         self._send = send |         self._send = send | ||||||
| @@ -141,7 +146,9 @@ class Lifespan: | |||||||
|         ) + self.asgi_app.sanic_app.listeners.get("after_server_start", []) |         ) + self.asgi_app.sanic_app.listeners.get("after_server_start", []) | ||||||
|  |  | ||||||
|         for handler in listeners: |         for handler in listeners: | ||||||
|             response = handler(self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop) |             response = handler( | ||||||
|  |                 self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop | ||||||
|  |             ) | ||||||
|             if isawaitable(response): |             if isawaitable(response): | ||||||
|                 await response |                 await response | ||||||
|  |  | ||||||
| @@ -159,7 +166,9 @@ class Lifespan: | |||||||
|         ) + self.asgi_app.sanic_app.listeners.get("after_server_stop", []) |         ) + self.asgi_app.sanic_app.listeners.get("after_server_stop", []) | ||||||
|  |  | ||||||
|         for handler in listeners: |         for handler in listeners: | ||||||
|             response = handler(self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop) |             response = handler( | ||||||
|  |                 self.asgi_app.sanic_app, self.asgi_app.sanic_app.loop | ||||||
|  |             ) | ||||||
|             if isawaitable(response): |             if isawaitable(response): | ||||||
|                 await response |                 await response | ||||||
|  |  | ||||||
| @@ -204,13 +213,19 @@ class ASGIApp: | |||||||
|                 for key, value in scope.get("headers", []) |                 for key, value in scope.get("headers", []) | ||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|         instance.do_stream = True if headers.get("expect") == "100-continue" else False |         instance.do_stream = ( | ||||||
|  |             True if headers.get("expect") == "100-continue" else False | ||||||
|  |         ) | ||||||
|         instance.lifespan = Lifespan(instance) |         instance.lifespan = Lifespan(instance) | ||||||
|  |  | ||||||
|         if scope["type"] == "lifespan": |         if scope["type"] == "lifespan": | ||||||
|             await instance.lifespan(scope, receive, send) |             await instance.lifespan(scope, receive, send) | ||||||
|         else: |         else: | ||||||
|             path = scope["path"][1:] if scope["path"].startswith("/") else scope["path"] |             path = ( | ||||||
|  |                 scope["path"][1:] | ||||||
|  |                 if scope["path"].startswith("/") | ||||||
|  |                 else scope["path"] | ||||||
|  |             ) | ||||||
|             url = "/".join([scope.get("root_path", ""), quote(path)]) |             url = "/".join([scope.get("root_path", ""), quote(path)]) | ||||||
|             url_bytes = url.encode("latin-1") |             url_bytes = url.encode("latin-1") | ||||||
|             url_bytes += b"?" + scope["query_string"] |             url_bytes += b"?" + scope["query_string"] | ||||||
| @@ -233,11 +248,18 @@ class ASGIApp: | |||||||
|  |  | ||||||
|             request_class = sanic_app.request_class or Request |             request_class = sanic_app.request_class or Request | ||||||
|             instance.request = request_class( |             instance.request = request_class( | ||||||
|                 url_bytes, headers, version, method, instance.transport, sanic_app, |                 url_bytes, | ||||||
|  |                 headers, | ||||||
|  |                 version, | ||||||
|  |                 method, | ||||||
|  |                 instance.transport, | ||||||
|  |                 sanic_app, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             if sanic_app.is_request_stream: |             if sanic_app.is_request_stream: | ||||||
|                 is_stream_handler = sanic_app.router.is_stream_handler(instance.request) |                 is_stream_handler = sanic_app.router.is_stream_handler( | ||||||
|  |                     instance.request | ||||||
|  |                 ) | ||||||
|                 if is_stream_handler: |                 if is_stream_handler: | ||||||
|                     instance.request.stream = StreamBuffer( |                     instance.request.stream = StreamBuffer( | ||||||
|                         sanic_app.config.REQUEST_BUFFER_QUEUE_SIZE |                         sanic_app.config.REQUEST_BUFFER_QUEUE_SIZE | ||||||
| @@ -287,13 +309,17 @@ class ASGIApp: | |||||||
|         callback = None if self.ws else self.stream_callback |         callback = None if self.ws else self.stream_callback | ||||||
|         await handler(self.request, None, 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: |     async def stream_callback(self, response: HTTPResponse) -> None: | ||||||
|         """ |         """ | ||||||
|         Write the response. |         Write the response. | ||||||
|         """ |         """ | ||||||
|         headers: List[Tuple[bytes, bytes]] = [] |         headers: List[Tuple[bytes, bytes]] = [] | ||||||
|         cookies: Dict[str, str] = {} |         cookies: Dict[str, str] = {} | ||||||
|  |         content_length: List[str] = [] | ||||||
|         try: |         try: | ||||||
|  |             content_length = response.headers.popall("content-length", []) | ||||||
|             cookies = { |             cookies = { | ||||||
|                 v.key: v |                 v.key: v | ||||||
|                 for _, v in list( |                 for _, v in list( | ||||||
| @@ -316,7 +342,9 @@ class ASGIApp: | |||||||
|                 type(response), |                 type(response), | ||||||
|             ) |             ) | ||||||
|             exception = ServerError("Invalid response type") |             exception = ServerError("Invalid response type") | ||||||
|             response = self.sanic_app.error_handler.response(self.request, exception) |             response = self.sanic_app.error_handler.response( | ||||||
|  |                 self.request, exception | ||||||
|  |             ) | ||||||
|             headers = [ |             headers = [ | ||||||
|                 (str(name).encode("latin-1"), str(value).encode("latin-1")) |                 (str(name).encode("latin-1"), str(value).encode("latin-1")) | ||||||
|                 for name, value in response.headers.items() |                 for name, value in response.headers.items() | ||||||
| @@ -324,14 +352,28 @@ class ASGIApp: | |||||||
|             ] |             ] | ||||||
|  |  | ||||||
|         response.asgi = True |         response.asgi = True | ||||||
|  |         is_streaming = isinstance(response, StreamingHTTPResponse) | ||||||
|         if "content-length" not in response.headers and not isinstance( |         if is_streaming and getattr(response, "chunked", False): | ||||||
|             response, StreamingHTTPResponse |             # disable sanic chunking, this is done at the ASGI-server level | ||||||
|         ): |             setattr(response, "chunked", False) | ||||||
|             headers += [(b"content-length", str(len(response.body)).encode("latin-1"))] |             # 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(getattr(response, "body", b""))).encode("latin-1"), | ||||||
|  |                 ) | ||||||
|  |             ] | ||||||
|  |  | ||||||
|         if "content-type" not in response.headers: |         if "content-type" not in response.headers: | ||||||
|             headers += [(b"content-type", str(response.content_type).encode("latin-1"))] |             headers += [ | ||||||
|  |                 (b"content-type", str(response.content_type).encode("latin-1")) | ||||||
|  |             ] | ||||||
|  |  | ||||||
|         if response.cookies: |         if response.cookies: | ||||||
|             cookies.update( |             cookies.update( | ||||||
| @@ -343,7 +385,8 @@ class ASGIApp: | |||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         headers += [ |         headers += [ | ||||||
|             (b"set-cookie", cookie.encode("utf-8")) for k, cookie in cookies.items() |             (b"set-cookie", cookie.encode("utf-8")) | ||||||
|  |             for k, cookie in cookies.items() | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|         await self.transport.send( |         await self.transport.send( | ||||||
|   | |||||||
| @@ -80,6 +80,8 @@ class StreamingHTTPResponse(BaseHTTPResponse): | |||||||
|         if type(data) != bytes: |         if type(data) != bytes: | ||||||
|             data = self._encode_body(data) |             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: |         if self.chunked: | ||||||
|             await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) |             await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) | ||||||
|         else: |         else: | ||||||
|   | |||||||
| @@ -235,7 +235,7 @@ def test_chunked_streaming_returns_correct_content(streaming_app): | |||||||
| @pytest.mark.asyncio | @pytest.mark.asyncio | ||||||
| async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): | async def test_chunked_streaming_returns_correct_content_asgi(streaming_app): | ||||||
|     request, response = await streaming_app.asgi_client.get("/") |     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): | def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user