WSX Protocol
Version: 1.0.0 Status: SOURCE OF TRUTH Last Updated: 2025-12-03
Overview
WSX (WebSocket eXtended) is a message format for RPC over WebSocket and NATS. It brings HTTP-like semantics to message-based transports.
Important: WSX is a protocol/format specification.
Request handling uses WsRequest(BaseRequest) from request.py.
Motivation
Different transports have different APIs:
HTTP: method, path, headers, cookies, query, body
WebSocket: just binary/text messages
NATS: subject, payload bytes, reply-to
WSX defines a message format that encapsulates HTTP-like semantics:
HTTP Request ─────┐
│
WebSocket RPC ─────┼──► BaseRequest ──► Handler ──► BaseResponse
│
NATS Message ─────┘
Message Format
Prefix
WSX messages start with WSX:// followed by JSON:
WSX://{"id":"...","method":"...","path":"...","headers":{},"data":{}}
WSX Request
WSX://{
"id": "uuid-123",
"method": "POST",
"path": "/users/42",
"headers": {
"content-type": "application/json",
"authorization": "Bearer xxx"
},
"cookies": {
"session_id": "xyz-789"
},
"query": {
"limit": "10::L",
"active": "true::B"
},
"data": {
"name": "Mario",
"birth": "1990-05-15::D"
}
}
Request Fields
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
Correlation ID |
|
string |
Yes |
GET, POST, PUT, DELETE, PATCH |
|
string |
Yes |
Routing path (e.g., “/users/42”) |
|
object |
No |
HTTP headers as dict |
|
object |
No |
Cookies as dict |
|
object |
No |
Query parameters (TYTX supported) |
|
any |
No |
Request payload (TYTX supported) |
WSX Response
WSX://{
"id": "uuid-123",
"status": 200,
"headers": {
"content-type": "application/json",
"x-request-id": "trace-456"
},
"cookies": {
"session_id": {
"value": "new-xyz",
"max_age": "3600::L",
"httponly": "true::B"
}
},
"data": {
"id": "42::L",
"message": "User created"
}
}
Response Fields
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
Same correlation ID as request |
|
int |
Yes |
HTTP status code (200, 404, 500, etc.) |
|
object |
No |
Response headers |
|
object |
No |
Set-Cookie equivalents |
|
any |
No |
Response payload |
TYTX Integration
Values support TYTX type suffixes for type preservation:
"price": "99.50::N" → Decimal("99.50")
"date": "2025-01-15::D" → date(2025, 1, 15)
"count": "42::L" → 42 (int)
"active": "true::B" → True (bool)
WSX parsing automatically hydrates these values via from_tytx().
Transport Flow
WebSocket (ASGI)
1. WebSocket receive() → text message
2. Detect WSX:// prefix or ::JS marker
3. Parse via from_tytx() → hydrates TYTX values automatically
4. Create WsRequest(BaseRequest)
5. Handler processes → result
6. WsResponse.send() → to_tytx() → WebSocket send()
NATS (Future)
1. NATS subscribe callback → msg
2. msg.data contains WSX://... or ::JS marker
3. Parse via from_tytx() → hydrates TYTX values automatically
4. Create NatsRequest(BaseRequest)
5. Handler processes → result
6. NatsResponse.send() → to_tytx() → nc.publish(msg.reply, ...)
Module Structure
The wsx/ directory contains protocol code only:
src/genro_asgi/wsx/
├── __init__.py
├── protocol.py # Parse/serialize WSX messages
└── handler.py # Route WSX messages to handlers
Request classes are in request.py, not in wsx/.
Protocol Parsing
# wsx/protocol.py - using genro-tytx API
WSX_PREFIX = "WSX://"
def parse_wsx_message(raw: str | bytes) -> dict[str, Any]:
"""
Parse WSX message string.
Args:
raw: Raw message (WSX:// prefix or ::JS marker)
Returns:
Parsed message dict with hydrated TYTX values
Raises:
ValueError: If not a valid WSX message
"""
if isinstance(raw, bytes):
# Binary data - try msgpack via from_tytx
from genro_tytx import from_tytx
return dict(from_tytx(raw, transport="msgpack"))
# String data
if raw.startswith(WSX_PREFIX):
raw = raw[len(WSX_PREFIX):]
# from_tytx handles ::JS marker and hydration automatically
from genro_tytx import from_tytx
return dict(from_tytx(raw))
def serialize_wsx_response(
request_id: str,
status: int,
data: Any,
headers: dict[str, str] | None = None,
cookies: dict[str, Any] | None = None,
) -> str:
"""
Serialize response to WSX format.
Returns:
JSON string with ::JS marker (handled by to_tytx)
"""
from genro_tytx import to_tytx
response = {
"id": request_id,
"status": status,
"data": data,
}
if headers:
response["headers"] = headers
if cookies:
response["cookies"] = cookies
# to_tytx includes ::JS marker automatically
return to_tytx(response)
Error Handling
Errors returned as response with status:
WSX://{
"id": "uuid-123",
"status": 404,
"data": {
"error": "User not found",
"code": "USER_NOT_FOUND"
}
}
Streaming
For streaming responses:
Multiple WSX responses with same
id"stream": trueindicates more messages follow"stream": falseor absent indicates final message
WSX://{"id": "123", "status": 200, "data": {"chunk": 1}, "stream": true}
WSX://{"id": "123", "status": 200, "data": {"chunk": 2}, "stream": true}
WSX://{"id": "123", "status": 200, "data": {"chunk": 3}, "stream": false}
Correlation ID
HTTP: Generated by server or from
x-request-idheaderWebSocket: Required in WSX message
idfieldNATS: Uses native reply-to for routing,
idfor application tracing
Transport-Agnostic Handler
async def get_user(request: BaseRequest) -> dict:
"""Works with HTTP, WebSocket, or NATS."""
user_id = request.path.split("/")[-1]
auth = request.headers.get("authorization")
session = request.cookies.get("session_id")
user = await db.get_user(user_id)
return {
"id": user.id,
"name": user.name,
"transport": request.transport, # "http", "websocket", or "nats"
}
Copyright: Softwell S.r.l. (2025) License: Apache License 2.0