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

@ -53,13 +53,14 @@ client.Send("Hello World", 11);
std::vector<char> data = client.RecieveSync();
```
The host is resolved for IPv4 and IPv6 alike and every address it resolves to is tried in turn, as Python's `socket.create_connection` does, so `localhost` reaches a server that listens on `::1` only. The optional third argument is a connect timeout, applied to each address on its own. A failed resolve, connect, send or receive throws `SocketErrorException`: still a `std::runtime_error` with the same message, but carrying a `kind` (`SocketErrorKind::System` for an errno such as `ECONNREFUSED`, `SocketErrorKind::Resolver` for a getaddrinfo result such as `EAI_NONAME`) and the `code`, so a caller can report the cause without parsing the text.
The host is resolved for IPv4 and IPv6 alike and every address it resolves to is tried in turn, as Python's `socket.create_connection` does, so `localhost` reaches a server that listens on `::1` only. The optional third argument is a connect timeout, applied to each address on its own. A failed resolve, connect, send or receive throws `SocketErrorException`: a `std::system_error`, so still a `std::runtime_error` with the same message, whose `code()` is the errno under `std::generic_category()` or the `getaddrinfo` result under `Crafter::ResolverCategory()`, and whose `phase` names the step (`SocketPhase::Resolve`, `Open`, `Connect`, `Send` or `Receive`), so a caller can report the cause without parsing the text.
```cpp
try {
Crafter::ClientTCP client("localhost", 5432, std::chrono::seconds(5));
} catch (const Crafter::SocketErrorException& e) {
std::println("[Errno {}] {}", e.code, std::strerror(e.code)); // [Errno 111] Connection refused
if (e.code() == std::errc::connection_refused) std::println("nothing listens there");
std::println("[Errno {}] {}", e.code().value(), e.code().message()); // [Errno 111] Connection refused
} catch (const Crafter::SocketTimeoutException&) {
std::println("timed out");
}