From 7541349422b37e6bf97d3c6cdd0e162105132d2a Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Wed, 10 Apr 2024 20:33:54 -0400 Subject: [PATCH] handle http errors better and add access logging --- basgi/__main__.py | 9 +++++++++ basgi/application.py | 33 +++++++++++++++++++++++++++++++-- basgi/error.py | 4 ++-- basgi/response.py | 27 +++++++++++++++++++++++++-- basgi/router.py | 14 +++++++++++++- 5 files changed, 80 insertions(+), 7 deletions(-) diff --git a/basgi/__main__.py b/basgi/__main__.py index dff4e0b..91134a0 100644 --- a/basgi/__main__.py +++ b/basgi/__main__.py @@ -1,8 +1,17 @@ from .application import Application +from .request import Request +from .response import Response +from .router import post app = Application( "test", "basgi.application:handle_run_app", "0.0.0.0", dev = True, reload_dirs = ["basgi"] ) + +@app.router.route("/heck", methods = "POST") +async def handle_heck(request: Request) -> Response: + return Response(200, "UvU") + + app.run() diff --git a/basgi/application.py b/basgi/application.py index 8f9756d..6b69483 100644 --- a/basgi/application.py +++ b/basgi/application.py @@ -1,5 +1,6 @@ import asyncio import os +import traceback import uvicorn from collections.abc import Awaitable, Callable @@ -71,14 +72,22 @@ class Application: if response is None: raise HttpError(500, "Empty response") + await self.on_request.handle_emit(request) + except InvalidMethodError: response = Response(405, request.method) except NotFoundError: response = Response(404, request.path) - except HttpError as e: - response = Response(e.status, e.message, headers = e.headers) + except HttpError as error: + response = Response.new_from_http_error(error) + + except Exception: + response = Response(500, "Internal Server Error") + traceback.print_exc() + + self.print_access_log(request, response) await send({ "type": "http.response.start", @@ -114,6 +123,26 @@ class Application: pass + def print_access_log(self, request: Request, response: Response) -> None: + message = "{}: {} \"{} {}\" {} {} \"{}\"".format( + "INFO", + request.headers.get( + "X-Forwarded-For", + request.headers.get( + "X-Real-Ip", + request.remote or "n/a" + ) + ), + request.method, + request.path, + response.status.value, + int(response.headers.get("Content-Length", 0)), + request.headers.get("User-Agent", "n/a") + ) + + print(message, flush = True) + + def run(self) -> None: try: asyncio.run(self.handle_run_server()) diff --git a/basgi/error.py b/basgi/error.py index 54a58b8..e2285e5 100644 --- a/basgi/error.py +++ b/basgi/error.py @@ -1,6 +1,6 @@ from typing import Any, Self -from .enum import HttpStatus +from .enums import HttpStatus class HttpError(Exception): @@ -86,4 +86,4 @@ class HttpError(Exception): @property def reason(self) -> str: - return self.status.reason # type: ignore + return self.status.reason diff --git a/basgi/response.py b/basgi/response.py index 2e38fa5..2d8dcc9 100644 --- a/basgi/response.py +++ b/basgi/response.py @@ -1,6 +1,7 @@ from multidict import CIMultiDict -from typing import Any +from typing import Any, Self +from .error import HttpError from .misc import convert_to_bytes @@ -11,14 +12,36 @@ class Response: mimetype: str | None = None, headers: dict[str, str] | None = None) -> None: + self._body = b"" + self.status: int = status - self.body: bytes = convert_to_bytes(body) self.headers: CIMultiDict[str] = CIMultiDict(headers or {}) + if body: + self.body = body + if mimetype: self.mimetype = mimetype + @classmethod + def new_from_http_error(cls: type[Self], error: HttpError) -> Self: + message = f"HTTP Error {error.status.value}: {error.message}" + return cls(error.status, message, headers = error.headers) + + + @property + def body(self) -> bytes: + return self._body + + + @body.setter + def body(self, value: Any) -> None: + self._body = convert_to_bytes(value) + self.headers.popall("Content-Length", None) + self.headers["Content-Length"] = str(len(self._body)) + + @property def mimetype(self) -> str: return self.headers["Content-Type"] diff --git a/basgi/router.py b/basgi/router.py index 5969f76..6e9be4d 100644 --- a/basgi/router.py +++ b/basgi/router.py @@ -1,8 +1,10 @@ import http_router from collections.abc import Awaitable, Callable, Iterable +from http_router.routes import RouteMatch from typing import Any, Pattern +from .error import HttpError from .request import Request from .response import Response @@ -14,7 +16,17 @@ class Router(http_router.Router): def __init__(self, name: str, **kwargs: Any): http_router.Router.__init__(self, **kwargs) self.name: str = name - set_router(self) + + + def __call__(self, *args: Any, **kwargs: Any) -> RouteMatch: + try: + return http_router.Router.__call__(self, *args, **kwargs) + + except http_router.NotFoundError as e: + raise HttpError(404, e.args[1]) from None + + except http_router.InvalidMethodError as e: + raise HttpError(405, e.args[0]) from None def __repr__(self) -> str: