# 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
