Skip to content

Throw ProtocolError on packet decoding failures - #564

Merged
slabko merged 3 commits into
masterfrom
rework-decoding-error-exceptions
Sep 4, 2026
Merged

Throw ProtocolError on packet decoding failures#564
slabko merged 3 commits into
masterfrom
rework-decoding-error-exceptions

Conversation

@slabko

@slabko slabko commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

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 throws ProtocolError with a message identifying the packet that failed to decode.

Behavior changes

  • 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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() throw ProtocolError on 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.

Comment thread clickhouse/client.cpp
Comment on lines 1233 to 1239
} 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;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello happens only during the handshake, and yes, in that case the client throws another exception.

Comment thread clickhouse/client.cpp
}

case ServerCodes::Exception: {
ServerError ret{std::make_shared<Exception>()};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is okay for this rare exception path.

Comment thread clickhouse/client.cpp

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().
@slabko
slabko force-pushed the rework-decoding-error-exceptions branch from f182e6e to 5671a8a Compare September 4, 2026 17:26
Comment thread clickhouse/client.cpp

if (!WireFormat::ReadVarint64(*input_, &packet_type)) {
return {};
throw ProtocolError{"can't read packet type from input stream"};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be use different messages to mark point where it is failing?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@slabko
slabko merged commit 564e5e9 into master Sep 4, 2026
88 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants