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.

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

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