feat(tcp): socket errors as std::system_error, with the phase that failed

SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.

Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.

Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
This commit is contained in:
Jorijn van der Graaf 2026-10-31 20:33:37 +01:00
commit e86c3b70a0
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
9 changed files with 107 additions and 63 deletions

View file

@ -44,9 +44,21 @@ namespace {
[[noreturn]] void ThrowRecieveError() {
const int failure = errno;
if (failure == EAGAIN || failure == EWOULDBLOCK) throw SocketTimeoutException();
throw SocketErrorException(SocketErrorKind::System, failure, std::strerror(failure));
throw SocketErrorException(SocketPhase::Receive, std::error_code(failure, std::generic_category()));
}
// getaddrinfo's results as an error category: the number is one of
// netdb.h's EAI_* codes and the text is what gai_strerror makes of it.
class ResolverErrorCategory : public std::error_category {
public:
const char* name() const noexcept override {
return "resolver";
}
std::string message(int code) const override {
return gai_strerror(code);
}
};
// One connect attempt to one address: a connected, blocking socket, or a
// throw that leaves nothing open.
int ConnectTo(const sockaddr* address, socklen_t length, std::chrono::milliseconds timeout) {
@ -55,7 +67,7 @@ namespace {
// An address family the host cannot open — IPv6 switched off,
// say — fails this address alone; the caller moves on.
const int failure = errno;
throw SocketErrorException(SocketErrorKind::System, failure, std::string("Could not open socket: ") + std::strerror(failure));
throw SocketErrorException(SocketPhase::Open, std::error_code(failure, std::generic_category()), "Could not open socket");
}
// With a timeout the connect runs non-blocking, so poll can bound the
@ -93,7 +105,7 @@ namespace {
// connected to anything — every later send/recv on it would fail
// with a far less obvious error.
close(fd);
throw SocketErrorException(SocketErrorKind::System, failure, std::string("Could not connect to server: ") + std::strerror(failure));
throw SocketErrorException(SocketPhase::Connect, std::error_code(failure, std::generic_category()), "Could not connect to server");
}
if (bounded) {
fcntl(fd, F_SETFL, flags);
@ -102,6 +114,11 @@ namespace {
}
}
const std::error_category& Crafter::ResolverCategory() noexcept {
static const ResolverErrorCategory category;
return category;
}
ClientTCP::ClientTCP(int socketid) : socketid(socketid)
{
@ -125,8 +142,8 @@ ClientTCP::ClientTCP(const char* hostName, std::uint16_t port, std::chrono::mill
const std::string service = std::to_string(port);
const int status = getaddrinfo(hostName, service.c_str(), &hints, &resolved);
if (status != 0 || resolved == nullptr) {
throw SocketErrorException(SocketErrorKind::Resolver, status,
std::string("Could not resolve host '") + hostName + "': " + gai_strerror(status));
throw SocketErrorException(SocketPhase::Resolve, std::error_code(status, ResolverCategory()),
std::string("Could not resolve host '") + hostName + "'");
}
for (const addrinfo* entry = resolved; entry != nullptr; entry = entry->ai_next) {
Address address {};
@ -215,7 +232,7 @@ void ClientTCP::Send(const void* buffer, std::uint32_t size) const {
} else if (status < 0) {
if (errno == EINTR) continue;
const int failure = errno;
throw SocketErrorException(SocketErrorKind::System, failure, std::strerror(failure));
throw SocketErrorException(SocketPhase::Send, std::error_code(failure, std::generic_category()));
}
sent += static_cast<std::uint32_t>(status);
}