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

id

string

Yes

Correlation ID

method

string

Yes

GET, POST, PUT, DELETE, PATCH

path

string

Yes

Routing path (e.g., “/users/42”)

headers

object

No

HTTP headers as dict

cookies

object

No

Cookies as dict

query

object

No

Query parameters (TYTX supported)

data

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

id

string

Yes

Same correlation ID as request

status

int

Yes

HTTP status code (200, 404, 500, etc.)

headers

object

No

Response headers

cookies

object

No

Set-Cookie equivalents

data

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": true indicates more messages follow

  • "stream": false or 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-id header

  • WebSocket: Required in WSX message id field

  • NATS: Uses native reply-to for routing, id for 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