Fix accept() error handling on Windows (#2561)

The accept loop in Server::listen_internal() classified accept() failures
by reading errno, but Winsock reports them through WSAGetLastError() and
never touches the CRT errno. Both retry branches were therefore dead code
on Windows, and every accept() failure fell through to the fatal path,
which closes the listening socket and ends listen().

That is reachable in normal operation: a peer resetting a pending
connection before it is accepted is enough, and descriptor or buffer
exhaustion shows up under load. One such event stopped the server from
accepting anything again.

Add is_accept_resource_error() and is_accept_transient_error() next to
is_connection_error(), which already abstracts the same errno vs
WSAGetLastError() difference, and use them in the accept loop.

The POSIX sets are widened to match the Windows ones rather than being
left as they were: ECONNABORTED is the POSIX spelling of the aborted
pending connection that motivates this, and ENFILE, ENOBUFS and ENOMEM
are resource exhaustion in the same sense as EMFILE.
This commit is contained in:
yhirose
2026-08-26 01:16:01 -04:00
committed by GitHub
parent 84f75185fe
commit f9c205632d
2 changed files with 101 additions and 4 deletions
+59
View File
@@ -355,6 +355,65 @@ TEST(SetSocketOptTest, TcpNoDelay) {
detail::close_socket(sock);
}
TEST(AcceptErrorTest, RetryableFailuresAreClassifiedPerPlatform) {
// accept() reports failures through WSAGetLastError() on Windows and through
// errno everywhere else. Asking the wrong one is what made the accept loop
// treat every recoverable failure as fatal on Windows.
struct Classification {
bool resource;
bool transient;
};
auto classify = [](int err) {
#ifdef _WIN32
WSASetLastError(err);
#else
errno = err;
#endif
// Read both before asserting: an assertion may clobber the error slot.
Classification c;
c.resource = detail::is_accept_resource_error();
c.transient = detail::is_accept_transient_error();
return c;
};
#ifdef _WIN32
const std::vector<int> resource_errors = {WSAEMFILE, WSAENOBUFS};
const std::vector<int> transient_errors = {WSAEINTR, WSAEWOULDBLOCK,
WSAECONNRESET, WSAECONNABORTED};
const std::vector<int> fatal_errors = {WSAENOTSOCK, WSAEINVAL, WSAEOPNOTSUPP};
#else
const std::vector<int> resource_errors = {EMFILE, ENFILE, ENOBUFS, ENOMEM};
const std::vector<int> transient_errors = {EINTR, EAGAIN, EWOULDBLOCK,
ECONNABORTED};
const std::vector<int> fatal_errors = {EBADF, EINVAL, ENOTSOCK};
#endif
for (auto err : resource_errors) {
const auto c = classify(err);
EXPECT_TRUE(c.resource) << "error " << err;
EXPECT_FALSE(c.transient) << "error " << err;
}
for (auto err : transient_errors) {
const auto c = classify(err);
EXPECT_TRUE(c.transient) << "error " << err;
EXPECT_FALSE(c.resource) << "error " << err;
}
for (auto err : fatal_errors) {
const auto c = classify(err);
EXPECT_FALSE(c.resource) << "error " << err;
EXPECT_FALSE(c.transient) << "error " << err;
}
#ifdef _WIN32
WSASetLastError(0);
#else
errno = 0;
#endif
}
TEST(ClientTest, MoveConstructible) {
EXPECT_FALSE(std::is_copy_constructible<Client>::value);
EXPECT_TRUE(std::is_nothrow_move_constructible<Client>::value);