# Request System **Version**: 2.0.0 **Status**: SOURCE OF TRUTH **Last Updated**: 2026-03-29 --- ## Overview The request system provides transport-agnostic request handling. All classes live in `request.py`: - `BaseRequest` - Abstract base class - `HttpRequest` - ASGI HTTP adapter (stream-based, body via receive) - `MsgRequest` - Message-based adapter (WSX over WebSocket, NATS) - `RequestRegistry` - Factory and tracking for active requests - `REQUEST_FACTORIES` - Default factory mapping - `get_current_request()` / `set_current_request()` - ContextVar access --- ## Class Hierarchy ``` BaseRequest (ABC) │ ├── HttpRequest # ASGI HTTP scope + body stream │ └── MsgRequest # WSX atomic messages ├─ transport_type="websocket" (WSX over WebSocket) └─ transport_type="nats" (WSX over NATS, future) ``` **HttpRequest**: for HTTP requests. Body arrives as a stream from the ASGI `receive` callable. Parsing delegated to `genro_tytx.asgi_data()`. **MsgRequest**: for message-based protocols (WSX). The message arrives as a complete unit — no streaming. WebSocket and NATS are specializations of the same WSX protocol, distinguished by `transport_type`. Both expose the same `BaseRequest` interface, enabling transport-agnostic dispatch: a single handler works identically for HTTP, WebSocket, or NATS. --- ## Request/Response Lifecycle Every request has an associated `Response` object (`request.response`), created automatically during `__init__`. ``` ASGI scope arrives │ ▼ RequestRegistry.create(scope, receive, send) ├─ factory() # sync: allocate slots (__init__) ├─ request.init() # async: read body, parse data └─ register in _requests dict │ ▼ set_current_request(request) │ ▼ Dispatcher resolves handler via router.node(path) │ ▼ result = handler(**request.query) │ ▼ response.set_result(result, node.metadata) │ ▼ await response(scope, receive, send) # sends ASGI response │ ▼ set_current_request(None) registry.unregister() ``` ### The init() Pattern Request creation is a two-step process: 1. `__init__()` — synchronous, allocates slots and creates the `Response` 2. `init()` — asynchronous, performs I/O (reads body, parses data) `RequestRegistry.create()` calls both in sequence. This is the standard genro-asgi pattern for separating structure from I/O. --- ## BaseRequest (ABC) Abstract interface for all request types. **Abstract properties** (subclasses must implement): | Property | Type | Description | |----------|------|-------------| | `id` | `str` | Server-generated correlation ID (UUID or x-request-id) | | `method` | `str` | HTTP method: GET, POST, PUT, DELETE, PATCH | | `path` | `str` | Request path (e.g., '/users/42') | | `headers` | `dict[str, str]` | Request headers (lowercase keys) | | `cookies` | `dict[str, str]` | Request cookies | | `query` | `dict[str, Any]` | Query parameters | | `data` | `Any` | Parsed body (dict for JSON, None if empty) | | `transport` | `str` | Transport type: 'http', 'websocket', 'nats' | **Concrete attributes** (on all requests): | Attribute | Type | Description | |-----------|------|-------------| | `response` | `Response` | Associated response object | | `auth_tags` | `list[str]` | Security tags (injected by AuthMiddleware) | | `env_capabilities` | `list[str]` | Environment flags (from scope) | | `external_id` | `str \| None` | Client-provided correlation ID | | `tytx_mode` | `bool` | True when request uses TYTX serialization | | `tytx_transport` | `str \| None` | TYTX transport: 'json', 'msgpack', or None | | `app_name` | `str \| None` | App handling this request (set after routing) | | `created_at` | `float` | Epoch timestamp when request was created | | `age` | `float` | Seconds since creation (computed) | --- ## HttpRequest ASGI HTTP adapter. Parses body via `genro_tytx.asgi_data()`. Supports both TYTX-encoded requests (with type hydration for Decimal, date, etc.) and standard HTTP requests with plain JSON. TYTX mode is detected from the `X-TYTX-Transport` header; this information is also used by `Response` to serialize the reply in the same format. ### Parsing Flow (init) ``` scope + receive arrive │ ├─ Decode headers from scope (latin-1) ├─ Detect X-TYTX-Transport header → set tytx_mode │ ├─ await asgi_data(scope, receive) │ ├─ headers (with optional TYTX hydration) │ ├─ query (parsed from query_string) │ ├─ cookies (parsed from Cookie header) │ └─ body (JSON or TYTX, auto-detected) │ ├─ Request ID from x-request-id header or UUID └─ auth_tags, env_capabilities from scope ``` ### Extra Properties | Property | Type | Description | |----------|------|-------------| | `scope` | `Scope` | Raw ASGI scope dict | | `body` | `bytes` | Raw body bytes (see limitation below) | | `scheme` | `str` | URL scheme: http or https | | `url` | `URL` | Full request URL object | | `headers_obj` | `Headers` | Case-insensitive Headers object | | `query_params` | `QueryParams` | QueryParams object | | `client` | `Address \| None` | Client address (host, port) | | `state` | `State` | Request-scoped state container | | `content_type` | `str \| None` | Content-Type header shortcut | ### Known Limitation: Raw Body `asgi_data()` consumes the ASGI `receive` callable internally. Raw body bytes are not preserved after parsing — `self.body` returns `b""`. This affects use cases like HMAC signature verification or raw logging. --- ## MsgRequest Message-based adapter for WSX protocol (WebSocket, NATS). WSX messages arrive as complete units — no streaming. The message is parsed by `_parse_wsx_message()` which handles: - Binary data → msgpack via `from_tytx` - `WSX://` prefix → stripped - `::JS` suffix → TYTX JSON with type markers - Other strings → standard `json.loads` ### TYTX Detection TYTX mode is detected from the message content: - Marker `::JS` at end of message body, or - `"tytx"` in the content-type header within the message ### Validation MsgRequest requires `id` and `method` fields in the WSX message. Missing fields raise `ValueError`. ### Extra Properties | Property | Type | Description | |----------|------|-------------| | `scope` | `Scope` | Raw ASGI scope dict | | `websocket` | `WebSocket \| None` | Underlying WebSocket connection | | `client` | `tuple \| None` | Client address (host, port) | --- ## RequestRegistry Factory and tracking for active requests. ```python registry = RequestRegistry() request = await registry.create(scope, receive, send) # ... handle request ... registry.unregister() # removes current request ``` ### Factory Mechanism `create()` looks up `scope["type"]` in the factories dict, instantiates the request class, and calls `init()`: ```python factory = self.factories[scope_type] # HttpRequest or MsgRequest request = factory() # sync __init__ await request.init(scope, receive, send) # async I/O self._requests[request.id] = request ``` ### Custom Factories ```python registry.register_factory("nats", NatsRequest) ``` --- ## REQUEST_FACTORIES Default mapping: ```python REQUEST_FACTORIES: dict[str, type[BaseRequest]] = { "http": HttpRequest, "websocket": MsgRequest, } ``` --- ## Handler Interaction Handlers access the current request via `get_current_request()` and control the response through `request.response`: ```python from genro_asgi.request import get_current_request @route() async def my_handler(self): request = get_current_request() # Read parsed body payload = request.data # dict for JSON, None if empty # Read auth context tags = request.auth_tags # ["read", "write"] # Control response request.response.set_header("X-Custom", "value") # Return value → Dispatcher calls response.set_result() return {"status": "ok"} ``` ### Notification Pattern (Fire-and-Forget) For protocols where some messages expect no response payload (e.g., JSON-RPC 2.0 notifications), the handler sets the status code and returns None: ```python @route() async def mcp_handler(self): request = get_current_request() envelope = request.data # JSON-RPC notification: no "id" field → no response expected if "id" not in envelope: request.response.status_code = 202 return None # Dispatcher sends HTTP 202 with empty body # Normal request: process and return JSON-RPC response result = await self._dispatch(envelope) return {"jsonrpc": "2.0", "id": envelope["id"], "result": result} ``` HTTP always requires a response at the transport level. The 202 status code signals "accepted, no content" — it is a transport acknowledgment, not a protocol-level reply. --- ## TYTX Support TYTX (Typed Data Interchange) preserves Python types across the wire: Decimal, date, datetime, time. ### Detection by Transport | Transport | Detection | Mechanism | |-----------|-----------|-----------| | HTTP | `X-TYTX-Transport` header | Checked in `HttpRequest.init()` | | WSX | `::JS` marker or content-type | Checked in `MsgRequest._parse_wsx_message()` | ### Impact on Parsing - **TYTX request**: `asgi_data()` / `from_tytx()` hydrate typed values (e.g., `"99.99::N"` → `Decimal("99.99")`) - **Standard request**: values pass through as plain strings/JSON types ### Impact on Response `Response.set_result()` checks `request.tytx_mode`: - If True → serializes with `to_tytx()` using the same transport - If False → serializes as standard JSON --- **Copyright**: Softwell S.r.l. (2025-2026) **License**: Apache License 2.0