diff --git a/sanic/app.py b/sanic/app.py index ec9027b5..07c5a9e8 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -30,6 +30,7 @@ from typing import ( List, Optional, Set, + Tuple, Type, Union, ) @@ -411,7 +412,13 @@ class Sanic(BaseSanic): self.websocket_enabled = enable - def blueprint(self, blueprint, **options): + def blueprint( + self, + blueprint: Union[ + Blueprint, List[Blueprint], Tuple[Blueprint], BlueprintGroup + ], + **options: Any, + ): """Register a blueprint on the application. :param blueprint: Blueprint object or (list, tuple) thereof @@ -869,7 +876,7 @@ class Sanic(BaseSanic): *, debug: bool = False, auto_reload: Optional[bool] = None, - ssl: Union[dict, SSLContext, None] = None, + ssl: Union[Dict[str, str], SSLContext, None] = None, sock: Optional[socket] = None, workers: int = 1, protocol: Optional[Type[Protocol]] = None, @@ -999,7 +1006,7 @@ class Sanic(BaseSanic): port: Optional[int] = None, *, debug: bool = False, - ssl: Union[dict, SSLContext, None] = None, + ssl: Union[Dict[str, str], SSLContext, None] = None, sock: Optional[socket] = None, protocol: Type[Protocol] = None, backlog: int = 100, diff --git a/sanic/blueprints.py b/sanic/blueprints.py index da26e3dd..f57475bf 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -181,10 +181,10 @@ class Blueprint(BaseSanic): @staticmethod def group( - *blueprints, - url_prefix="", - version=None, - strict_slashes=None, + *blueprints: Union[Blueprint, BlueprintGroup], + url_prefix: Optional[str] = None, + version: Optional[Union[int, str, float]] = None, + strict_slashes: Optional[bool] = None, version_prefix: str = "/v", ): """ diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 1b823e8b..16cd684d 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -31,6 +31,7 @@ class NotFound(SanicException): """ status_code = 404 + quiet = True class InvalidUsage(SanicException): @@ -39,6 +40,7 @@ class InvalidUsage(SanicException): """ status_code = 400 + quiet = True class MethodNotSupported(SanicException): @@ -47,6 +49,7 @@ class MethodNotSupported(SanicException): """ status_code = 405 + quiet = True def __init__(self, message, method, allowed_methods): super().__init__(message) @@ -70,6 +73,7 @@ class ServiceUnavailable(SanicException): """ status_code = 503 + quiet = True class URLBuildError(ServerError): @@ -101,6 +105,7 @@ class RequestTimeout(SanicException): """ status_code = 408 + quiet = True class PayloadTooLarge(SanicException): @@ -109,6 +114,7 @@ class PayloadTooLarge(SanicException): """ status_code = 413 + quiet = True class HeaderNotFound(InvalidUsage): @@ -117,6 +123,7 @@ class HeaderNotFound(InvalidUsage): """ status_code = 400 + quiet = True class ContentRangeError(SanicException): @@ -125,6 +132,7 @@ class ContentRangeError(SanicException): """ status_code = 416 + quiet = True def __init__(self, message, content_range): super().__init__(message) @@ -137,6 +145,7 @@ class HeaderExpectationFailed(SanicException): """ status_code = 417 + quiet = True class Forbidden(SanicException): @@ -145,6 +154,7 @@ class Forbidden(SanicException): """ status_code = 403 + quiet = True class InvalidRangeType(ContentRangeError): @@ -153,6 +163,7 @@ class InvalidRangeType(ContentRangeError): """ status_code = 416 + quiet = True class PyFileError(Exception): @@ -196,6 +207,7 @@ class Unauthorized(SanicException): """ status_code = 401 + quiet = True def __init__(self, message, status_code=None, scheme=None, **kwargs): super().__init__(message, status_code) diff --git a/sanic/http.py b/sanic/http.py index a594e60c..16603c38 100644 --- a/sanic/http.py +++ b/sanic/http.py @@ -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 diff --git a/setup.py b/setup.py index 3bdd68b0..fabe6c28 100644 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ ujson = "ujson>=1.35" + env_dependency uvloop = "uvloop>=0.5.3" + env_dependency types_ujson = "types-ujson" + env_dependency requirements = [ - "sanic-routing==0.7.0", + "sanic-routing~=0.7", "httptools>=0.0.10", uvloop, ujson, @@ -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", diff --git a/tests/test_pipelining.py b/tests/test_pipelining.py new file mode 100644 index 00000000..689a787b --- /dev/null +++ b/tests/test_pipelining.py @@ -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"] diff --git a/tests/test_static.py b/tests/test_static.py index d702ca69..00e5611d 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -471,7 +471,7 @@ def test_stack_trace_on_not_found(app, static_file_directory, caplog): assert response.status == 404 assert counter[logging.INFO] == 5 - assert counter[logging.ERROR] == 1 + assert counter[logging.ERROR] == 0 def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):