Source code for httk.web.runtime.asgi

import json
from urllib.parse import parse_qsl

from starlette.applications import Starlette
from starlette.datastructures import UploadFile
from starlette.requests import Request
from starlette.responses import Response
from starlette.routing import Route

from httk.web.engine.site_engine import SiteEngine
from httk.web.model.errors import WebError
from httk.web.model.request import HttpRequestContext

[docs] MAX_POST_BODY_BYTES = 1_000_000
async def _handle_request(request: Request) -> Response: engine: SiteEngine = request.app.state.engine route = request.path_params.get("path", "") try: request_context = HttpRequestContext( method=request.method, query=dict(request.query_params), postvars=await _extract_postvars(request), headers={k.lower(): v for k, v in request.headers.items()}, ) result = engine.render(route, request=request_context) except WebError as exc: return Response(content=str(exc), status_code=exc.status_code, media_type="text/plain") return Response(content=result.body, status_code=result.status_code, media_type=result.content_type)
[docs] def create_app(*, engine: SiteEngine, debug: bool = False) -> Starlette: app = Starlette(debug=debug, routes=[Route("/{path:path}", _handle_request, methods=["GET", "POST"])]) app.state.engine = engine return app
async def _extract_postvars(request: Request) -> dict[str, str]: if request.method.upper() != "POST": return {} content_type = request.headers.get("content-type", "").lower() content_length = request.headers.get("content-length") if content_length is not None: try: if int(content_length) > MAX_POST_BODY_BYTES: raise WebError( f"POST body too large (>{MAX_POST_BODY_BYTES} bytes).", status_code=413, ) except ValueError: # Ignore invalid content-length values and attempt best-effort parsing. pass if "application/x-www-form-urlencoded" in content_type: raw = await request.body() if len(raw) > MAX_POST_BODY_BYTES: raise WebError( f"POST body too large (>{MAX_POST_BODY_BYTES} bytes).", status_code=413, ) body = raw.decode("utf-8", errors="replace") return {k: v for k, v in parse_qsl(body, keep_blank_values=True)} if "application/json" in content_type: raw = await request.body() if len(raw) > MAX_POST_BODY_BYTES: raise WebError( f"POST body too large (>{MAX_POST_BODY_BYTES} bytes).", status_code=413, ) try: payload = json.loads(raw.decode("utf-8", errors="replace")) except json.JSONDecodeError: return {} if isinstance(payload, dict): out_json: dict[str, str] = {} for key, value in payload.items(): if isinstance(value, (str, int, float, bool)) or value is None: out_json[str(key)] = "" if value is None else str(value) return out_json return {} if "multipart/form-data" in content_type: try: form = await request.form() except Exception: return {} out_form: dict[str, str] = {} for key, value in form.multi_items(): normalized_key = str(key) if isinstance(value, UploadFile): # Keep uploads explicit and compact in postvars. out_form[normalized_key] = value.filename or "" else: out_form[normalized_key] = str(value) return out_form return {}