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 classHttpRequest- ASGI HTTP adapter (stream-based, body via receive)MsgRequest- Message-based adapter (WSX over WebSocket, NATS)RequestRegistry- Factory and tracking for active requestsREQUEST_FACTORIES- Default factory mappingget_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:
__init__()— synchronous, allocates slots and creates theResponseinit()— 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 |
|---|---|---|
|
|
Server-generated correlation ID (UUID or x-request-id) |
|
|
HTTP method: GET, POST, PUT, DELETE, PATCH |
|
|
Request path (e.g., ‘/users/42’) |
|
|
Request headers (lowercase keys) |
|
|
Request cookies |
|
|
Query parameters |
|
|
Parsed body (dict for JSON, None if empty) |
|
|
Transport type: ‘http’, ‘websocket’, ‘nats’ |
Concrete attributes (on all requests):
Attribute |
Type |
Description |
|---|---|---|
|
|
Associated response object |
|
|
Security tags (injected by AuthMiddleware) |
|
|
Environment flags (from scope) |
|
|
Client-provided correlation ID |
|
|
True when request uses TYTX serialization |
|
|
TYTX transport: ‘json’, ‘msgpack’, or None |
|
|
App handling this request (set after routing) |
|
|
Epoch timestamp when request was created |
|
|
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 |
|---|---|---|
|
|
Raw ASGI scope dict |
|
|
Raw body bytes (see limitation below) |
|
|
URL scheme: http or https |
|
|
Full request URL object |
|
|
Case-insensitive Headers object |
|
|
QueryParams object |
|
|
Client address (host, port) |
|
|
Request-scoped state container |
|
|
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_tytxWSX://prefix → stripped::JSsuffix → TYTX JSON with type markersOther strings → standard
json.loads
TYTX Detection
TYTX mode is detected from the message content:
Marker
::JSat 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 |
|---|---|---|
|
|
Raw ASGI scope dict |
|
|
Underlying WebSocket connection |
|
|
Client address (host, port) |
RequestRegistry
Factory and tracking for active requests.
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():
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
registry.register_factory("nats", NatsRequest)
REQUEST_FACTORIES
Default mapping:
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:
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:
@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 |
|
Checked in |
WSX |
|
Checked in |
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 transportIf False → serializes as standard JSON
Copyright: Softwell S.r.l. (2025-2026) License: Apache License 2.0