handle http errors better and add access logging

This commit is contained in:
Izalia Mae 2024-04-10 20:33:54 -04:00
parent d74dd18ed2
commit 7541349422
5 changed files with 80 additions and 7 deletions

View file

@ -1,8 +1,17 @@
from .application import Application from .application import Application
from .request import Request
from .response import Response
from .router import post
app = Application( app = Application(
"test", "basgi.application:handle_run_app", "0.0.0.0", dev = True, reload_dirs = ["basgi"] "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() app.run()

View file

@ -1,5 +1,6 @@
import asyncio import asyncio
import os import os
import traceback
import uvicorn import uvicorn
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
@ -71,14 +72,22 @@ class Application:
if response is None: if response is None:
raise HttpError(500, "Empty response") raise HttpError(500, "Empty response")
await self.on_request.handle_emit(request)
except InvalidMethodError: except InvalidMethodError:
response = Response(405, request.method) response = Response(405, request.method)
except NotFoundError: except NotFoundError:
response = Response(404, request.path) response = Response(404, request.path)
except HttpError as e: except HttpError as error:
response = Response(e.status, e.message, headers = e.headers) 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({ await send({
"type": "http.response.start", "type": "http.response.start",
@ -114,6 +123,26 @@ class Application:
pass 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: def run(self) -> None:
try: try:
asyncio.run(self.handle_run_server()) asyncio.run(self.handle_run_server())

View file

@ -1,6 +1,6 @@
from typing import Any, Self from typing import Any, Self
from .enum import HttpStatus from .enums import HttpStatus
class HttpError(Exception): class HttpError(Exception):
@ -86,4 +86,4 @@ class HttpError(Exception):
@property @property
def reason(self) -> str: def reason(self) -> str:
return self.status.reason # type: ignore return self.status.reason

View file

@ -1,6 +1,7 @@
from multidict import CIMultiDict from multidict import CIMultiDict
from typing import Any from typing import Any, Self
from .error import HttpError
from .misc import convert_to_bytes from .misc import convert_to_bytes
@ -11,14 +12,36 @@ class Response:
mimetype: str | None = None, mimetype: str | None = None,
headers: dict[str, str] | None = None) -> None: headers: dict[str, str] | None = None) -> None:
self._body = b""
self.status: int = status self.status: int = status
self.body: bytes = convert_to_bytes(body)
self.headers: CIMultiDict[str] = CIMultiDict(headers or {}) self.headers: CIMultiDict[str] = CIMultiDict(headers or {})
if body:
self.body = body
if mimetype: if mimetype:
self.mimetype = 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 @property
def mimetype(self) -> str: def mimetype(self) -> str:
return self.headers["Content-Type"] return self.headers["Content-Type"]

View file

@ -1,8 +1,10 @@
import http_router import http_router
from collections.abc import Awaitable, Callable, Iterable from collections.abc import Awaitable, Callable, Iterable
from http_router.routes import RouteMatch
from typing import Any, Pattern from typing import Any, Pattern
from .error import HttpError
from .request import Request from .request import Request
from .response import Response from .response import Response
@ -14,7 +16,17 @@ class Router(http_router.Router):
def __init__(self, name: str, **kwargs: Any): def __init__(self, name: str, **kwargs: Any):
http_router.Router.__init__(self, **kwargs) http_router.Router.__init__(self, **kwargs)
self.name: str = name 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: def __repr__(self) -> str: