//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // A failed resolve or connect throws SocketErrorException, which carries the // code it failed with: the errno for a connect, the getaddrinfo result for a // name. Before, both were a bare std::runtime_error, so a caller that reports // the cause in its own words — a Python-style "[Errno 111] Connection // refused" in front of a language model reading the output — had to parse the // message back apart. The message itself stays what it always was, and the // exception is still a std::runtime_error, so existing handlers keep working. #include #include #include #include #include #include #include #include import Crafter.Network; import std; using namespace Crafter; using namespace std::chrono_literals; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } // A port on 127.0.0.1 nothing listens on: bound by the kernel, then released. std::uint16_t ClosedPort() { const int fd = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in address {}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); bind(fd, reinterpret_cast(&address), sizeof(address)); socklen_t length = sizeof(address); getsockname(fd, reinterpret_cast(&address), &length); close(fd); return ntohs(address.sin_port); } } // namespace int main() { { bool thrown = false; try { ClientTCP client("127.0.0.1", ClosedPort(), 2s); } catch (const SocketErrorException& e) { thrown = true; Check(e.kind == SocketErrorKind::System, "a refused connect is a system error"); Check(e.code == ECONNREFUSED, std::format("it carries ECONNREFUSED (got {})", e.code)); Check(std::string_view(e.what()) == std::string("Could not connect to server: ") + std::strerror(ECONNREFUSED), std::format("its message is the one ClientTCP always threw (got '{}')", e.what())); } Check(thrown, "a refused connect throws SocketErrorException"); } { bool caught = false; try { ClientTCP client("127.0.0.1", ClosedPort(), 2s); } catch (const std::runtime_error&) { caught = true; } Check(caught, "it is still caught as std::runtime_error"); } { // .invalid never resolves (RFC 6761); without a network the resolver // fails differently (EAI_AGAIN), but always as a resolver error. bool thrown = false; try { ClientTCP client("no-such-host.invalid", 80, 2s); } catch (const SocketErrorException& e) { thrown = true; Check(e.kind == SocketErrorKind::Resolver, "a name that does not resolve is a resolver error"); Check(e.code != 0, std::format("it carries the getaddrinfo code (got {})", e.code)); Check(std::string_view(e.what()).starts_with("Could not resolve host 'no-such-host.invalid': "), std::format("its message names the host (got '{}')", e.what())); } Check(thrown, "an unresolvable host throws SocketErrorException"); } std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }