Everything but CustomServer OK.
This commit is contained in:
		| @@ -1028,6 +1028,8 @@ class Sanic: | ||||
|                     response = await self._run_response_middleware( | ||||
|                         request, response, request_name=name | ||||
|                     ) | ||||
|                 except CancelledError: | ||||
|                     raise | ||||
|                 except Exception: | ||||
|                     error_logger.exception( | ||||
|                         "Exception occurred in one of response " | ||||
| @@ -1043,6 +1045,8 @@ class Sanic: | ||||
|                     f"Invalid response type {response!r} (need HTTPResponse)" | ||||
|                 ) | ||||
|  | ||||
|         except CancelledError: | ||||
|             raise | ||||
|         except Exception as e: | ||||
|             # -------------------------------------------- # | ||||
|             # Response Generation Failed | ||||
|   | ||||
							
								
								
									
										376
									
								
								sanic/http.py
									
									
									
									
									
								
							
							
						
						
									
										376
									
								
								sanic/http.py
									
									
									
									
									
								
							| @@ -29,6 +29,24 @@ class Stage(Enum): | ||||
| HTTP_CONTINUE = b"HTTP/1.1 100 Continue\r\n\r\n" | ||||
|  | ||||
| class Http: | ||||
|     __slots__ = [ | ||||
|         "_send", | ||||
|         "_receive_more", | ||||
|         "recv_buffer", | ||||
|         "protocol", | ||||
|         "expecting_continue", | ||||
|         "stage", | ||||
|         "keep_alive", | ||||
|         "head_only", | ||||
|         "request", | ||||
|         "exception", | ||||
|         "url", | ||||
|         "request_chunked", | ||||
|         "request_bytes_left", | ||||
|         "response", | ||||
|         "response_func", | ||||
|         "response_bytes_left", | ||||
|     ] | ||||
|     def __init__(self, protocol): | ||||
|         self._send = protocol.send | ||||
|         self._receive_more = protocol.receive_more | ||||
| @@ -40,8 +58,46 @@ class Http: | ||||
|         self.head_only = None | ||||
|         self.request = None | ||||
|         self.exception = None | ||||
|         self.url = None | ||||
|  | ||||
|     async def http1_receive_request(self): | ||||
|     async def http1(self): | ||||
|         """HTTP 1.1 connection handler""" | ||||
|         while True:  # As long as connection stays keep-alive | ||||
|             try: | ||||
|                 # Receive and handle a request | ||||
|                 self.stage = Stage.REQUEST | ||||
|                 self.response_func = self.http1_response_start | ||||
|                 await self.http1_request_header() | ||||
|                 await self.protocol.request_handler(self.request) | ||||
|                 # Handler finished, response should've been sent | ||||
|                 if self.stage is Stage.HANDLER: | ||||
|                     raise ServerError("Handler produced no response") | ||||
|                 if self.stage is Stage.RESPONSE: | ||||
|                     await self.send(end_stream=True) | ||||
|                 # Consume any remaining request body (TODO: or disconnect?) | ||||
|                 if self.request_bytes_left or self.request_chunked: | ||||
|                     logger.error(f"{self.request} body not consumed.") | ||||
|                     async for _ in self: | ||||
|                         pass | ||||
|             except CancelledError: | ||||
|                 # Write an appropriate response before exiting | ||||
|                 e = self.exception or ServiceUnavailable(f"Cancelled") | ||||
|                 self.exception = None | ||||
|                 self.keep_alive = False | ||||
|                 await self.error_response(e) | ||||
|             except Exception as e: | ||||
|                 # Write an error response | ||||
|                 await self.error_response(e) | ||||
|             # Exit and disconnect if finished | ||||
|             if self.stage is not Stage.IDLE or not self.keep_alive: | ||||
|                 break | ||||
|             # Wait for next request | ||||
|             if not self.recv_buffer: | ||||
|                 await self._receive_more() | ||||
|  | ||||
|     async def http1_request_header(self): | ||||
|         """Receive and parse request header into self.request.""" | ||||
|         # Receive until full header is in buffer | ||||
|         buf = self.recv_buffer | ||||
|         pos = 0 | ||||
|         while len(buf) < self.protocol.request_max_size: | ||||
| @@ -55,105 +111,195 @@ class Http: | ||||
|                 self.stage = Stage.REQUEST | ||||
|         else: | ||||
|             raise PayloadTooLarge("Payload Too Large") | ||||
|  | ||||
|         self.protocol._total_request_size = pos + 4 | ||||
|  | ||||
|        # Parse header content | ||||
|         try: | ||||
|             reqline, *headers = buf[:pos].decode().split("\r\n") | ||||
|             reqline, *raw_headers = buf[:pos].decode().split("\r\n") | ||||
|             method, self.url, protocol = reqline.split(" ") | ||||
|             if protocol not in ("HTTP/1.0", "HTTP/1.1"): | ||||
|             if protocol == "HTTP/1.1": | ||||
|                 self.keep_alive = True | ||||
|             elif protocol == "HTTP/1.0": | ||||
|                 self.keep_alive = False | ||||
|             else: | ||||
|                 raise Exception | ||||
|             self.head_only = method.upper() == "HEAD" | ||||
|             headers = Header( | ||||
|                 (name.lower(), value.lstrip()) | ||||
|                 for name, value in (h.split(":", 1) for h in headers) | ||||
|             ) | ||||
|             body = False | ||||
|             headers = [] | ||||
|             for name, value in (h.split(":", 1) for h in raw_headers): | ||||
|                 name, value = h = name.lower(), value.lstrip() | ||||
|                 if name in ("content-length", "transfer-encoding"): | ||||
|                     body = True | ||||
|                 elif name == "connection": | ||||
|                     self.keep_alive = value.lower() == "keep-alive" | ||||
|                 headers.append(h) | ||||
|         except: | ||||
|             raise InvalidUsage("Bad Request") | ||||
|         # Prepare a Request object | ||||
|         request = self.protocol.request_class( | ||||
|             url_bytes=self.url.encode(), | ||||
|             headers=headers, | ||||
|             headers=Header(headers), | ||||
|             version=protocol[-3:], | ||||
|             method=method, | ||||
|             transport=self.protocol.transport, | ||||
|             app=self.protocol.app, | ||||
|         ) | ||||
|  | ||||
|         # Prepare a request object from the header received | ||||
|         request.stream = self | ||||
|         self.protocol.state["requests_count"] += 1 | ||||
|         self.keep_alive = ( | ||||
|             protocol == "HTTP/1.1" | ||||
|             or headers.get("connection", "").lower() == "keep-alive" | ||||
|         ) | ||||
|         # Prepare for request body | ||||
|         body = headers.get("transfer-encoding") == "chunked" or int( | ||||
|             headers.get("content-length", 0) | ||||
|         ) | ||||
|         self.request_chunked = False | ||||
|         self.request_bytes_left = 0 | ||||
|         if body: | ||||
|             headers = request.headers | ||||
|             expect = headers.get("expect") | ||||
|             if expect: | ||||
|             if expect is not None: | ||||
|                 if expect.lower() == "100-continue": | ||||
|                     self.expecting_continue = True | ||||
|                 else: | ||||
|                     raise HeaderExpectationFailed( | ||||
|                         f"Unknown Expect: {expect}") | ||||
|                     raise HeaderExpectationFailed(f"Unknown Expect: {expect}") | ||||
|             request.stream = self | ||||
|             if body is True: | ||||
|             if headers.get("transfer-encoding") == "chunked": | ||||
|                 self.request_chunked = True | ||||
|                 pos -= 2  # One CRLF stays in buffer | ||||
|             else: | ||||
|                 self.request_bytes_left = body | ||||
|                 self.request_bytes_left = int(headers["content-length"]) | ||||
|         # Remove header and its trailing CRLF | ||||
|         del buf[: pos + 4] | ||||
|         self.stage = Stage.HANDLER | ||||
|         self.request = request | ||||
|  | ||||
|     async def http1(self): | ||||
|         """HTTP 1.1 connection handler""" | ||||
|         while self.stage is Stage.IDLE and self.keep_alive: | ||||
|             try: | ||||
|                 # Receive request header and call handler | ||||
|                 await self.http1_receive_request() | ||||
|                 await self.protocol.request_handler(self.request) | ||||
|                 if self.stage is Stage.HANDLER: | ||||
|                     raise ServerError("Handler produced no response") | ||||
|                 # Finish sending a response (if no error) | ||||
|                 if self.stage is Stage.RESPONSE: | ||||
|                     await self.send(end_stream=True) | ||||
|                 # Consume any remaining request body | ||||
|                 if self.request_bytes_left or self.request_chunked: | ||||
|                     logger.error(f"{self.request} body not consumed.") | ||||
|                     async for _ in self: | ||||
|                         pass | ||||
|             except CancelledError: | ||||
|                 # Exit after trying to finish a response | ||||
|                 self.keep_alive = False | ||||
|                 if self.exception is None: | ||||
|                     self.exception = ServiceUnavailable(f"Cancelled") | ||||
|             except Exception as e: | ||||
|                 self.exception = e | ||||
|             if self.exception: | ||||
|                 e, self.exception = self.exception, None | ||||
|                 # Exception while idle? Probably best to close connection | ||||
|                 if self.stage is Stage.IDLE: | ||||
|                     return | ||||
|                 # Request failure? Try to respond but then disconnect | ||||
|                 if self.stage is Stage.REQUEST: | ||||
|                     self.keep_alive = False | ||||
|                     self.stage = Stage.HANDLER | ||||
|                 # Return an error page if possible | ||||
|                 if self.stage is Stage.HANDLER: | ||||
|                     app = self.protocol.app | ||||
|                     response = await app.handle_exception(self.request, e) | ||||
|                     await self.respond(response).send(end_stream=True) | ||||
|     def http1_response_start(self, data, end_stream) -> bytes: | ||||
|         res = self.response | ||||
|         # Compatibility with simple response body | ||||
|         if not data and res.body: | ||||
|             data, end_stream = res.body, True | ||||
|         size = len(data) | ||||
|         status = res.status | ||||
|         headers = res.headers | ||||
|         if res.content_type and "content-type" not in headers: | ||||
|             headers["content-type"] = res.content_type | ||||
|         # Not Modified, Precondition Failed | ||||
|         if status in (304, 412): | ||||
|             headers = remove_entity_headers(headers) | ||||
|         if not has_message_body(status): | ||||
|             # Header-only response status | ||||
|             self.response_func = None | ||||
|             if ( | ||||
|                 data | ||||
|                 or not end_stream | ||||
|                 or "content-length" in headers | ||||
|                 or "transfer-encoding" in headers | ||||
|             ): | ||||
|                 # TODO: This matches old Sanic operation but possibly | ||||
|                 # an exception would be more appropriate? | ||||
|                 data, size, end_stream = b"", 0, True | ||||
|                 headers.pop("content-length", None) | ||||
|                 headers.pop("transfer-encoding", None) | ||||
|                 #raise ServerError( | ||||
|                 #    f"A {status} response may only have headers, no body." | ||||
|                 #) | ||||
|         elif self.head_only and "content-length" in headers: | ||||
|             self.response_func = None | ||||
|         elif end_stream: | ||||
|             # Non-streaming response (all in one block) | ||||
|             headers["content-length"] = size | ||||
|             self.response_func = None | ||||
|         elif "content-length" in headers: | ||||
|             # Streaming response with size known in advance | ||||
|             self.response_bytes_left = int(headers["content-length"]) - size | ||||
|             self.response_func = self.http1_response_normal | ||||
|         else: | ||||
|             # Length not known, use chunked encoding | ||||
|             headers["transfer-encoding"] = "chunked" | ||||
|             data = b"%x\r\n%b\r\n" % (size, data) if size else None | ||||
|             self.response_func = self.http1_response_chunked | ||||
|         if self.head_only: | ||||
|             # Head request: don't send body | ||||
|             data = b"" | ||||
|             self.response_func = self.head_response_ignored | ||||
|         headers["connection"] = "keep-alive" if self.keep_alive else "close" | ||||
|         ret = format_http1_response(status, headers.items(), data) | ||||
|         # Send a 100-continue if expected and not Expectation Failed | ||||
|         if self.expecting_continue: | ||||
|             self.expecting_continue = False | ||||
|             if status != 417: | ||||
|                 ret = HTTP_CONTINUE + ret | ||||
|         # Send response | ||||
|         self.log_response() | ||||
|         self.stage = Stage.IDLE if end_stream else Stage.RESPONSE | ||||
|         return ret | ||||
|  | ||||
|     def head_response_ignored(self, data, end_stream): | ||||
|         """HEAD response: body data silently ignored.""" | ||||
|         if end_stream: | ||||
|             self.response_func = None | ||||
|             self.stage = Stage.IDLE | ||||
|  | ||||
|     def http1_response_chunked(self, data, end_stream) -> bytes: | ||||
|         """Format a part of response body in chunked encoding.""" | ||||
|         # Chunked encoding | ||||
|         size = len(data) | ||||
|         if end_stream: | ||||
|             self.response_func = None | ||||
|             self.stage = Stage.IDLE | ||||
|             if size: | ||||
|                 return b"%x\r\n%b\r\n0\r\n\r\n" % (size, data) | ||||
|             return b"0\r\n\r\n" | ||||
|         return b"%x\r\n%b\r\n" % (size, data) if size else None | ||||
|  | ||||
|     def http1_response_normal(self, data: bytes, end_stream: bool) -> bytes: | ||||
|         """Format / keep track of non-chunked response.""" | ||||
|         self.response_bytes_left -= len(data) | ||||
|         if self.response_bytes_left <= 0: | ||||
|             if self.response_bytes_left < 0: | ||||
|                 raise ServerError("Response was bigger than content-length") | ||||
|             self.response_func = None | ||||
|             self.stage = Stage.IDLE | ||||
|         elif end_stream: | ||||
|             raise ServerError("Response was smaller than content-length") | ||||
|         return data | ||||
|  | ||||
|     async def error_response(self, exception): | ||||
|         # Disconnect after an error if in any other state than handler | ||||
|         if self.stage is not Stage.HANDLER: | ||||
|             self.keep_alive = False | ||||
|         # Request failure? Respond but then disconnect | ||||
|         if self.stage is Stage.REQUEST: | ||||
|             self.stage = Stage.HANDLER | ||||
|         # From request and handler states we can respond, otherwise be silent | ||||
|         if self.stage is Stage.HANDLER: | ||||
|             app = self.protocol.app | ||||
|             response = await app.handle_exception(self.request, exception) | ||||
|             await self.respond(response).send(end_stream=True) | ||||
|  | ||||
|     def log_response(self): | ||||
|         """ | ||||
|         Helper method provided to enable the logging of responses in case if | ||||
|         the :attr:`HttpProtocol.access_log` is enabled. | ||||
|  | ||||
|         :param response: Response generated for the current request | ||||
|  | ||||
|         :type response: :class:`sanic.response.HTTPResponse` or | ||||
|             :class:`sanic.response.StreamingHTTPResponse` | ||||
|  | ||||
|         :return: None | ||||
|         """ | ||||
|         if self.protocol.access_log: | ||||
|             req, res = self.request, self.response | ||||
|             extra = { | ||||
|                 "status": getattr(res, "status", 0), | ||||
|                 "byte": getattr(self, "response_bytes_left", -1), | ||||
|                 "host": "UNKNOWN", | ||||
|                 "request": "nil", | ||||
|             } | ||||
|             if req is not None: | ||||
|                 if req.ip: | ||||
|                     extra["host"] = f"{req.ip}:{req.port}" | ||||
|                 extra["request"] = f"{req.method} {req.url}" | ||||
|             access_logger.info("", extra=extra) | ||||
|  | ||||
|     # Request methods | ||||
|  | ||||
|     async def __aiter__(self): | ||||
|         """Async iterate over request body.""" | ||||
|         while True: | ||||
|             data = await self.read() | ||||
|             if not data: | ||||
| @@ -161,6 +307,7 @@ class Http: | ||||
|             yield data | ||||
|  | ||||
|     async def read(self): | ||||
|         """Read some bytes of request body.""" | ||||
|         # Send a 100-continue if needed | ||||
|         if self.expecting_continue: | ||||
|             self.expecting_continue = False | ||||
| @@ -227,105 +374,8 @@ class Http: | ||||
|         """ | ||||
|         if data is None and end_stream is None: | ||||
|             end_stream = True | ||||
|         data = self.data_to_send(data, end_stream) | ||||
|         if data is None: | ||||
|         data = data.encode() if hasattr(data, "encode") else data or b"" | ||||
|         data = self.response_func(data, end_stream) | ||||
|         if not data: | ||||
|             return | ||||
|         await self._send(data) | ||||
|  | ||||
|     def data_to_send(self, data, end_stream): | ||||
|         """Format output data bytes for given body data. | ||||
|         Headers are prepended to the first output block and then cleared. | ||||
|          :param data: str or bytes to be written | ||||
|          :return: bytes to send, or None if there is nothing to send | ||||
|         """ | ||||
|         data = data.encode() if hasattr(data, "encode") else data | ||||
|         size = len(data) if data is not None else 0 | ||||
|  | ||||
|         # Headers not yet sent? | ||||
|         if self.stage is Stage.HANDLER: | ||||
|             if self.response.body: | ||||
|                 data = self.response.body + data if data else self.response.body | ||||
|                 size = len(data) | ||||
|             r = self.response | ||||
|             status = r.status | ||||
|             headers = r.headers | ||||
|             if r.content_type and "content-type" not in headers: | ||||
|                 headers["content-type"] = r.content_type | ||||
|             # Not Modified, Precondition Failed | ||||
|             if status in (304, 412): | ||||
|                 headers = remove_entity_headers(headers) | ||||
|             if not has_message_body(status): | ||||
|                 # Header-only response status | ||||
|                 if ( | ||||
|                     size > 0 | ||||
|                     or not end_stream | ||||
|                     or "content-length" in headers | ||||
|                     or "transfer-encoding" in headers | ||||
|                 ): | ||||
|                     # TODO: This matches old Sanic operation but possibly | ||||
|                     # an exception would be more appropriate? | ||||
|                     data = None | ||||
|                     size = 0 | ||||
|                     end_stream = True | ||||
|                     #raise ServerError( | ||||
|                     #    f"A {status} response may only have headers, no body." | ||||
|                     #) | ||||
|             elif self.head_only and "content-length" in headers: | ||||
|                 pass | ||||
|             elif end_stream: | ||||
|                 # Non-streaming response (all in one block) | ||||
|                 headers["content-length"] = size | ||||
|             elif "content-length" in headers: | ||||
|                 # Streaming response with size known in advance | ||||
|                 self.response_bytes_left = int(headers["content-length"]) - size | ||||
|             else: | ||||
|                 # Length not known, use chunked encoding | ||||
|                 headers["transfer-encoding"] = "chunked" | ||||
|                 data = b"%x\r\n%b\r\n" % (size, data) if size else None | ||||
|                 self.response_bytes_left = True | ||||
|             self.headers = None | ||||
|             if self.head_only: | ||||
|                 data = None | ||||
|                 self.response_bytes_left = None | ||||
|             if self.keep_alive: | ||||
|                 headers["connection"] = "keep-alive" | ||||
|                 headers["keep-alive"] = self.protocol.keep_alive_timeout | ||||
|             else: | ||||
|                 headers["connection"] = "close" | ||||
|             ret = format_http1_response(status, headers.items(), data or b"") | ||||
|             # Send a 100-continue if expected and not Expectation Failed | ||||
|             if self.expecting_continue: | ||||
|                 self.expecting_continue = False | ||||
|                 if status != 417: | ||||
|                     ret = HTTP_CONTINUE + ret | ||||
|             # Send response | ||||
|             self.stage = Stage.IDLE if end_stream else Stage.RESPONSE | ||||
|             return ret | ||||
|  | ||||
|         # HEAD request: don't send body | ||||
|         if self.head_only: | ||||
|             return None | ||||
|  | ||||
|         if self.stage is not Stage.RESPONSE: | ||||
|             if size: | ||||
|                 raise RuntimeError("Cannot send data to a closed stream") | ||||
|             return | ||||
|  | ||||
|         # Chunked encoding | ||||
|         if self.response_bytes_left is True: | ||||
|             if end_stream: | ||||
|                 self.response_bytes_left = None | ||||
|                 self.stage = Stage.IDLE | ||||
|                 if size: | ||||
|                     return b"%x\r\n%b\r\n0\r\n\r\n" % (size, data) | ||||
|                 return b"0\r\n\r\n" | ||||
|             return b"%x\r\n%b\r\n" % (size, data) if size else None | ||||
|  | ||||
|         # Normal encoding | ||||
|         else: | ||||
|             self.response_bytes_left -= size | ||||
|             if self.response_bytes_left <= 0: | ||||
|                 if self.response_bytes_left < 0: | ||||
|                     raise ServerError("Response was bigger than content-length") | ||||
|                 self.stage = Stage.IDLE | ||||
|             return data if size else None | ||||
|   | ||||
| @@ -26,7 +26,7 @@ from sanic.exceptions import ( | ||||
|     ServiceUnavailable, | ||||
| ) | ||||
| from sanic.http import Http, Stage | ||||
| from sanic.log import access_logger, logger | ||||
| from sanic.log import logger | ||||
| from sanic.request import Request | ||||
|  | ||||
|  | ||||
| @@ -181,7 +181,7 @@ class HttpProtocol(asyncio.Protocol): | ||||
|                 stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED) | ||||
|                 and duration > self.response_timeout | ||||
|             ): | ||||
|                 self._http.exception = RequestTimeout("Response Timeout") | ||||
|                 self._http.exception = ServiceUnavailable("Response Timeout") | ||||
|             else: | ||||
|                 interval = min(self.keep_alive_timeout, self.request_timeout, self.response_timeout) / 2 | ||||
|                 self.loop.call_later(max(0.1, interval), self.check_timeouts) | ||||
|   | ||||
| @@ -79,12 +79,8 @@ class DelayableSanicTestClient(SanicTestClient): | ||||
|  | ||||
| request_timeout_default_app = Sanic("test_request_timeout_default") | ||||
| request_no_timeout_app = Sanic("test_request_no_timeout") | ||||
|  | ||||
| # Note: The delayed client pauses before making a request, so technically | ||||
| # it is in keep alive duration. Earlier Sanic versions entered a new connection | ||||
| # in request mode even if no bytes of request were received. | ||||
| request_timeout_default_app.config.KEEP_ALIVE_TIMEOUT = 0.6 | ||||
| request_no_timeout_app.config.KEEP_ALIVE_TIMEOUT = 0.6 | ||||
| request_timeout_default_app.config.REQUEST_TIMEOUT = 0.6 | ||||
| request_no_timeout_app.config.REQUEST_TIMEOUT = 0.6 | ||||
|  | ||||
|  | ||||
| @request_timeout_default_app.route("/1") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 L. Kärkkäinen
					L. Kärkkäinen