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

@ -37,26 +37,42 @@ namespace Crafter {
}
};
// Where the code of a SocketErrorException comes from.
export enum class SocketErrorKind {
// An errno: ECONNREFUSED, ENETUNREACH, EMFILE, ...
System,
// A getaddrinfo result: EAI_NONAME, EAI_AGAIN, ...
Resolver,
// The step of a connection a SocketErrorException failed at.
export enum class SocketPhase {
// getaddrinfo on the host name.
Resolve,
// socket() for an address the host resolved to.
Open,
// connect() to that address.
Connect,
Send,
Receive,
};
// A resolve, a socket(), a connect, a send or a receive that failed, with
// the code it failed with. what() is the text ClientTCP has always thrown, and catching
// std::runtime_error still catches it; the code is for callers that
// report the cause in their own words — a Python-style
// "[Errno 111] Connection refused", say — and would otherwise have to
// parse it back out of the message.
export class SocketErrorException : public std::runtime_error {
// The error category of getaddrinfo's results (EAI_NONAME, EAI_AGAIN,
// ...): a number space of its own that overlaps the errno values, so a
// resolver code is never confused with an errno, and whose message() is
// gai_strerror's text. One instance for the process, like
// std::generic_category().
export const std::error_category& ResolverCategory() noexcept;
// A resolve, a socket(), a connect, a send or a receive that failed. A
// std::system_error, so still a std::runtime_error for the handlers that
// catch one: code() is the errno under std::generic_category(), or the
// getaddrinfo result under ResolverCategory(), and what() is the text
// ClientTCP has always thrown, the code's own text after it. `phase`
// says which step failed. A caller that reports the cause in its own
// words — "[Errno 111] Connection refused", say — takes code().value()
// and code().message() instead of parsing what(), and compares code()
// with std::errc::connection_refused instead of the number.
export class SocketErrorException : public std::system_error {
public:
SocketErrorException(SocketErrorKind kind, std::int32_t code, const std::string& message)
: std::runtime_error(message), kind(kind), code(code) {}
SocketErrorKind kind;
std::int32_t code;
SocketErrorException(SocketPhase phase, std::error_code code, const std::string& what)
: std::system_error(code, what), phase(phase) {}
// Without a message of its own: what() is the code's text alone.
SocketErrorException(SocketPhase phase, std::error_code code)
: std::system_error(code), phase(phase) {}
SocketPhase phase;
};
export class ClientTCP {