diff --git a/adafruit_httpserver/response.py b/adafruit_httpserver/response.py index 1c5c0f9..5809bb8 100644 --- a/adafruit_httpserver/response.py +++ b/adafruit_httpserver/response.py @@ -29,7 +29,7 @@ class HTTPResponse: status: HTTPStatus headers: Dict[str, str] content_type: str - + cache: Optional[int] filename: Optional[str] root_path: str @@ -41,6 +41,7 @@ def __init__( # pylint: disable=too-many-arguments body: str = "", headers: Dict[str, str] = None, content_type: str = MIMEType.TYPE_TXT, + cache: Optional[int] = 0, filename: Optional[str] = None, root_path: str = "", http_version: str = "HTTP/1.1", @@ -54,6 +55,7 @@ def __init__( # pylint: disable=too-many-arguments self.body = body self.headers = headers or {} self.content_type = content_type + self.cache = cache self.filename = filename self.root_path = root_path self.http_version = http_version @@ -64,8 +66,10 @@ def _construct_response_bytes( # pylint: disable=too-many-arguments status: HTTPStatus = CommonHTTPStatus.OK_200, content_type: str = MIMEType.TYPE_TXT, content_length: Union[int, None] = None, + cache: int = 0, headers: Dict[str, str] = None, body: str = "", + chunked: bool = False, ) -> bytes: """Constructs the response bytes from the given parameters.""" @@ -75,12 +79,17 @@ def _construct_response_bytes( # pylint: disable=too-many-arguments response_headers = {} if headers is None else headers.copy() response_headers.setdefault("Content-Type", content_type) - response_headers.setdefault("Content-Length", content_length or len(body)) response_headers.setdefault("Connection", "close") + if chunked: + response_headers.setdefault("Transfer-Encoding", "chunked") + else: + response_headers.setdefault("Content-Length", content_length or len(body)) for header, value in response_headers.items(): response += f"{header}: {value}\r\n" + response += f"Cache-Control: max-age={cache}\r\n" + response += f"\r\n{body}" return response.encode("utf-8") @@ -116,6 +125,33 @@ def send(self, conn: Union["SocketPool.Socket", "socket.socket"]) -> None: body=self.body, ) + def send_chunk_headers( + self, conn: Union["SocketPool.Socket", "socket.socket"] + ) -> None: + """Send Headers for a chunked response over the given socket.""" + self._send_bytes( + conn, + self._construct_response_bytes( + status=self.status, + content_type=self.content_type, + chunked=True, + cache=self.cache, + body="", + ), + ) + + def send_body_chunk( + self, conn: Union["SocketPool.Socket", "socket.socket"], chunk: str + ) -> None: + """Send chunk of data to the given socket. Send an empty("") chunk to finish the session. + + :param Union["SocketPool.Socket", "socket.socket"] conn: Current connection. + :param str chunk: String data to be sent. + """ + size = "%X\r\n".encode() % len(chunk) + self._send_bytes(conn, size) + self._send_bytes(conn, chunk.encode() + b"\r\n") + def _send_response( # pylint: disable=too-many-arguments self, conn: Union["SocketPool.Socket", "socket.socket"], @@ -129,6 +165,7 @@ def _send_response( # pylint: disable=too-many-arguments self._construct_response_bytes( status=status, content_type=content_type, + cache=self.cache, headers=headers, body=body, ), @@ -148,6 +185,7 @@ def _send_file_response( # pylint: disable=too-many-arguments status=self.status, content_type=MIMEType.from_file_name(filename), content_length=file_length, + cache=self.cache, headers=headers, ), ) diff --git a/adafruit_httpserver/server.py b/adafruit_httpserver/server.py index 8bf1045..db953d6 100644 --- a/adafruit_httpserver/server.py +++ b/adafruit_httpserver/server.py @@ -53,6 +53,17 @@ def route_func(request): print("Received a request of length", len(raw_text), "bytes") return HTTPResponse(body="hello world") + + @server.route(path, method) + def route_func(request, conn): + raw_text = request.raw_request.decode("utf8") + print("Received a request of length", len(raw_text), "bytes") + res = HTTPResponse(content_type="text/html") + res.send_chunk_headers(conn) + res.send_body_chunk(conn, "Some content") + res.send_body_chunk(conn, "Some more content") + res.send_body_chunk(conn, "") # Send empty packet to finish chunked stream + return None # Return None, so server knows that nothing else needs to be sent. """ def route_decorator(func: Callable) -> Callable: @@ -162,12 +173,18 @@ def poll(self): # If a handler for route exists and is callable, call it. if handler is not None and callable(handler): - response = handler(request) + # Need to pass connection for chunked encoding to work. + try: + response = handler(request, conn) + except TypeError: + response = handler(request) + if response is None: + return # If no handler exists and request method is GET, try to serve a file. elif request.method == HTTPMethod.GET: response = HTTPResponse( - filename=request.path, root_path=self.root_path + filename=request.path, root_path=self.root_path, cache=604800 ) # If no handler exists and request method is not GET, return 400 Bad Request.