Throw ProtocolError on packet decoding failures - #564
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The implementation still surfaces some handshake decode failures as a generic “fail to connect” (not identifying the failed packet) which conflicts with the PR’s stated behavior, and there are a few low-risk allocation inefficiencies to address.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR improves ClickHouse client robustness by converting packet decode failures into ProtocolError exceptions instead of silently treating corrupted/incomplete responses as successful completion.
Changes:
- Make
ReceivePacket()throwProtocolErroron all packet decoding failures, with packet-specific context in error messages. - Refactor per-packet decoding into helper methods (
ReceiveProgress,ReceiveProfileInfo, etc.) to centralize validation and event emission. - Add unit tests that simulate corrupted server responses/handshake packets via a scripted socket.
File summaries
| File | Description |
|---|---|
clickhouse/client.cpp |
Changes packet decoding to throw ProtocolError on decode failures; refactors decoding logic into helper methods; adjusts exception handling during handshake and query response processing. |
ut/CMakeLists.txt |
Adds the new protocol-focused unit test source file to the UT target. |
ut/client_protocol_ut.cpp |
Adds tests covering malformed packet types and corrupt packet payloads to ensure decode failures raise ProtocolError (and well-formed exception packets still raise ServerException). |
Review details
Suppressed comments (1)
clickhouse/client.cpp:1234
- This pre-allocates an Exception object that is thrown away once ReceiveException succeeds (
error = ServerError(e);), causing an avoidable heap allocation during handshake failures.
ServerError ret{std::make_shared<Exception>()};
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } else if (packet_type == ServerCodes::Exception) { | ||
| ReceiveException(true); | ||
| return false; | ||
| ServerError ret{std::make_shared<Exception>()}; | ||
| if (!ReceiveException(ret)) { | ||
| throw ProtocolError{"server rejected the connection, but its exception packet could not be decoded"}; | ||
| } | ||
| throw ret; | ||
| } |
There was a problem hiding this comment.
Hello happens only during the handshake, and yes, in that case the client throws another exception.
| } | ||
|
|
||
| case ServerCodes::Exception: { | ||
| ServerError ret{std::make_shared<Exception>()}; |
There was a problem hiding this comment.
This is okay for this rare exception path.
|
|
||
| bool Client::Impl::ReceiveException(bool rethrow, ServerError * error) { | ||
| bool Client::Impl::ReceiveException(ServerError & error) { | ||
| std::shared_ptr<Exception> e(new Exception); |
Previously, when a packet payload could not be decoded (e.g. the connection dropped mid-response), ReceivePacket() returned an empty std::monostate variant that callers treated as a clean end of stream: NextBlock() returned std::nullopt as if the query had completed successfully, silently losing data. Now every decoding failure throws ProtocolError with a message identifying the packet that failed to decode, and std::monostate is removed from the DecodedPacket variant. Behavior changes: - A connection broken mid-query now surfaces as ProtocolError instead of a seemingly successful end of stream. This applies regardless of ClientOptions::SetRethrowException(false), which only concerns server-reported exceptions. - QueryEvents::OnServerException is no longer invoked with a partially populated exception when decoding the exception packet fails. - If the server rejects the connection but its exception packet cannot be decoded, the handshake now fails with a descriptive ProtocolError.
Move packet payload decoding out of ReceivePacket() into dedicated ReceiveProfileInfo/ReceiveProgress/ReceiveLog/ReceiveTableColumns/ ReceiveProfileEvents helpers. ReceiveException no longer throws; the throw decision (gated by ClientOptions::SetRethrowException) now lives at the call sites in ReceivePacket() and ReceiveHello().
f182e6e to
5671a8a
Compare
|
|
||
| if (!WireFormat::ReadVarint64(*input_, &packet_type)) { | ||
| return {}; | ||
| throw ProtocolError{"can't read packet type from input stream"}; |
There was a problem hiding this comment.
may be use different messages to mark point where it is failing?
There was a problem hiding this comment.
actually everything looks ok - never mind.
I usually like to see "read failed: packet type" - so I can grep on "read failed" and see what steps passed.
Summary
Previously, when a packet payload could not be decoded (e.g. the connection dropped mid-response),
ReceivePacket()returned as if the query had completed successfully, silently losing data. Now every decoding failure throwsProtocolErrorwith a message identifying the packet that failed to decode.Behavior changes
QueryEvents::OnServerExceptionis no longer invoked with a partially populated exception when decoding the exception packet fails.ProtocolError.