Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 additions & 1 deletion clickhouse/base/socket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,11 @@ size_t SocketInput::DoRead(void* buf, size_t len) {
}

if (ret == 0) {
throw std::system_error(getSocketErrorCode(), getErrorCategory(), "closed");
#if defined(_win_)
throw std::system_error(WSAECONNRESET, getErrorCategory(), "connection closed by peer");
#else
throw std::system_error(ECONNRESET, getErrorCategory(), "connection closed by peer");

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.

ECONNRESET = Connection reset by peer. Here, the connections are closed gracefully when unexpected. This requires a more thoughtful solution.

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.

Before I revise the error value, could you confirm what error representation you would prefer here? I want to preserve the existing std::system_error / retry behavior rather than introduce a new error abstraction without precedent.

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.

Honestly, I do not think it is a good idea to return std::system_error here because it suggests that the error occurred somewhere in a system call and that is not the case at all. The system call worked just fine and returned exactly what it was expected to return.

The error occurs at the application level during decoding because the decoder needs more data, but there is none. Maybe the server changed something, maybe a proxy dropped the connection, or maybe we are using a ClickHouse clone with a new protocol implementation.

Either way, this is a decoding error - a truncated data error, to be specific. I would use the ProtocolError exception type here.

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.

Thanks, that makes sense. I’ll revise the recv() == 0 path to treat the condition as a truncated protocol/decode error and use ProtocolError rather than std::system_error, while keeping actual socket/system-call failures on the existing path.

#endif
}

throw std::system_error(getSocketErrorCode(), getErrorCategory(), "can't receive string data");
Expand Down
53 changes: 53 additions & 0 deletions ut/socket_ut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,56 @@ TEST(Socketcase, connecttimeout) {
// auto input = socket.makeInputStream();
// input->Read(buffer, sizeof(buffer));
//}

#if !defined(_win_)
# include <sys/socket.h>
# include <unistd.h>

// Regression test for issue #487.
//
// On a clean peer close, `recv()` returns 0, which is EOF, not an error.
// POSIX does NOT require `errno` to be set when `recv()` returns 0, so reading
// `errno` at that point yields a stale value from a previous syscall. Prior
// to the fix, `SocketInput::DoRead` surfaced that stale `errno` to the caller
// (e.g. "Operation now in progress" if the last failing call was the
// non-blocking `connect()`), making the exception message non-deterministic
// and misleading.
//
// The fix reports `ECONNRESET` with a fixed message instead. This test
// drives a clean close via `socketpair(2)`, seeds `errno` to a known value
// that is NOT `ECONNRESET`, and asserts the resulting exception's
// `error_code` is exactly `ECONNRESET`.
TEST(Socketcase, recvReturnsZeroReportsConnResetNotStaleErrno) {
int sv[2];
ASSERT_EQ(0, ::socketpair(AF_UNIX, SOCK_STREAM, 0, sv));

// Seed `errno` to a known stale value that must NOT leak into the
// exception. On Linux, closing an invalid fd sets `errno = EBADF` (9),
// which is clearly distinct from `ECONNRESET` (104).
if (::close(-1) != -1) {
// Sanity guard: `close(-1)` must fail; if it doesn't, the test
// cannot guarantee `errno` is set as expected.
::close(sv[0]);
::close(sv[1]);
FAIL() << "close(-1) unexpectedly succeeded; cannot seed errno";
}
ASSERT_EQ(EBADF, errno);

SocketInput input(sv[0]);
// Close the peer side: the next `recv()` on sv[0] returns 0 (EOF).
::close(sv[1]);

char buf[16];
try {
input.Read(buf, sizeof(buf));
::close(sv[0]);
FAIL() << "expected std::system_error on clean peer close";
} catch (const std::system_error& e) {
::close(sv[0]);
EXPECT_EQ(ECONNRESET, e.code().value())
<< "stale errno leaked into the exception: " << e.code().value();
EXPECT_NE(EBADF, e.code().value())
<< "regression: stale errno was surfaced instead of ECONNRESET";
}
}
#endif // !defined(_win_)
Loading