diff --git a/src/pymax/protocol/models.py b/src/pymax/protocol/models.py index 9eb0049..edc401b 100644 --- a/src/pymax/protocol/models.py +++ b/src/pymax/protocol/models.py @@ -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): diff --git a/src/pymax/protocol/tcp/protocol.py b/src/pymax/protocol/tcp/protocol.py index 29a59dd..ad9d5fc 100644 --- a/src/pymax/protocol/tcp/protocol.py +++ b/src/pymax/protocol/tcp/protocol.py @@ -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, ) diff --git a/tests/protocol/test_protocols.py b/tests/protocol/test_protocols.py index 9a4ff53..ed2c833 100644 --- a/tests/protocol/test_protocols.py +++ b/tests/protocol/test_protocols.py @@ -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(