//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // A send or a receive that fails throws SocketErrorException with its errno // in code() and the phase that failed, as a failed connect does: a caller // that reports the cause in its own words — "the connection broke ([Errno // 104] Connection reset by peer)" — needs the code, and the message alone is // the strerror text a send and a receive always threw. A receive timeout is // still SocketTimeoutException and a clean close SocketClosedException. #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 listener on 127.0.0.1 that accepts one connection and resets it: SO_LINGER // with a zero timeout makes close() send an RST instead of a FIN. struct Resetter { int listener = -1; std::uint16_t port = 0; std::thread thread; Resetter() { listener = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in address {}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); bind(listener, reinterpret_cast(&address), sizeof(address)); listen(listener, 1); socklen_t length = sizeof(address); getsockname(listener, reinterpret_cast(&address), &length); port = ntohs(address.sin_port); thread = std::thread([this] { const int client = accept(listener, nullptr, nullptr); linger reset { .l_onoff = 1, .l_linger = 0 }; setsockopt(client, SOL_SOCKET, SO_LINGER, &reset, sizeof(reset)); close(client); }); } ~Resetter() { if (thread.joinable()) thread.join(); close(listener); } }; } // namespace int main() { { Resetter peer; ClientTCP client("127.0.0.1", peer.port, 2s); client.SetReceiveTimeout(2s); peer.thread.join(); bool thrown = false; try { client.RecieveSync(16); } catch (const SocketErrorException& e) { thrown = true; Check(e.phase == SocketPhase::Receive, "a reset connection fails in the receive phase"); Check(e.code() == std::errc::connection_reset && e.code().value() == ECONNRESET, std::format("it carries ECONNRESET (got {})", e.code().value())); Check(std::string_view(e.what()) == std::strerror(ECONNRESET), std::format("its message is the strerror text (got '{}')", e.what())); } catch (const std::exception& e) { Check(false, std::format("the receive threw something else: {}", e.what())); } Check(thrown, "a receive on a reset connection throws SocketErrorException"); } { Resetter peer; ClientTCP client("127.0.0.1", peer.port, 2s); peer.thread.join(); // The first send after the reset may still be accepted locally; the // kernel answers the next one with EPIPE or ECONNRESET. std::int32_t code = 0; bool runtime = false; bool sending = false; const std::string data(4096, 'x'); for (int i = 0; i < 50 && code == 0; ++i) { try { client.Send(data.data(), static_cast(data.size())); } catch (const SocketErrorException& e) { code = e.code().value(); sending = e.phase == SocketPhase::Send && e.code().category() == std::generic_category(); runtime = dynamic_cast(&e) != nullptr; } catch (const std::exception&) { break; } std::this_thread::sleep_for(10ms); } Check(code == EPIPE || code == ECONNRESET, std::format("a send into a reset connection throws SocketErrorException with EPIPE or ECONNRESET (got {})", code)); Check(sending && runtime, "in the send phase, an errno, and still a std::runtime_error"); } std::println("{}", failures == 0 ? "all passed" : std::format("{} failed", failures)); return failures == 0 ? 0 : 1; }