Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/pymax/protocol/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ class InboundFrame(BaseModel):
cmd: int = 0
seq: int | None = None
payload: dict[Any, Any] | None = None
raw: dict[Any, Any] | None = None
# Не каждый фрейм несёт map: ответ-ошибка может прийти голым значением
# (например, числовым кодом), поэтому raw хранит исходный payload как есть.
raw: Any = None


class TcpPacketHeader(BaseModel):
Expand Down
18 changes: 16 additions & 2 deletions src/pymax/protocol/tcp/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,28 @@ def decode(self, raw: bytes | str) -> InboundFrame:
packed_packet.header.flags,
packed_packet.header.payload_len,
)
payload = self.payload_decoder.decode(
decoded = self.payload_decoder.decode(
packed_packet.payload_bytes, flags=packed_packet.header.flags
)

# Не каждый фрейм несёт map: например, ответ-ошибка приходит голым
# значением (числовым кодом). payload оставляем строго dict|None — его
# так читают консьюмеры, — а исходное значение кладём в raw, чтобы не
# терять данные и не ронять весь приём кадров на ValidationError.
payload = decoded if isinstance(decoded, dict) else None
if payload is None and decoded is not None:
logger.debug(
"non-dict tcp payload opcode=%s cmd=%s type=%s value=%r",
packed_packet.header.opcode,
packed_packet.header.cmd,
type(decoded).__name__,
decoded,
)

return InboundFrame(
opcode=packed_packet.header.opcode,
cmd=packed_packet.header.cmd,
seq=packed_packet.header.seq,
payload=payload,
raw=payload,
raw=decoded,
)
23 changes: 23 additions & 0 deletions tests/protocol/test_protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,29 @@ def test_tcp_protocol_roundtrip() -> None:
)


def test_tcp_protocol_keeps_non_dict_payload_in_raw() -> None:
protocol = TcpProtocol()
framer = TcpPacketFramer()
# Сервер может прислать ответ-ошибку голым значением (не map). Раньше это
# роняло создание InboundFrame с ValidationError и убивало цикл приёма.
raw = framer.pack(
ver=protocol.version,
cmd=Command.ERROR,
seq=5,
opcode=Opcode.LOGIN,
flags=0,
payload_bytes=msgpack.packb(-12, use_bin_type=True),
)

decoded = protocol.decode(raw)

assert decoded.opcode == Opcode.LOGIN
assert decoded.cmd == Command.ERROR
assert decoded.seq == 5
assert decoded.payload is None
assert decoded.raw == -12


def test_tcp_protocol_supports_two_byte_sequence_ids() -> None:
protocol = TcpProtocol()
outbound = OutboundFrame(
Expand Down