//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // ClientHTTP1 bounds the TCP connect (connectTimeout) as well as every wait // for a response (timeout). Before, the connect had no bound of its own: a // host that drops the SYNs held Send() for the kernel's retries, about two // minutes, whatever `timeout` said — a readiness probe with a ten-second // budget could hang its caller for two minutes. // // Two servers on loopback. One accepts — the kernel completes the handshake // from its backlog — and never answers: the read timeout. The other has a // full accept queue, so Linux drops the SYNs of further connects: the connect // timeout. #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 socket listening on 127.0.0.1, on a port the kernel picks. struct LoopbackListener { int fd = -1; std::uint16_t port = 0; explicit LoopbackListener(int backlog) { 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)); listen(fd, backlog); socklen_t length = sizeof(address); getsockname(fd, reinterpret_cast(&address), &length); port = ntohs(address.sin_port); } ~LoopbackListener() { close(fd); } }; long long Milliseconds(std::chrono::steady_clock::duration duration) { return std::chrono::duration_cast(duration).count(); } } // namespace int main() { { LoopbackListener silent(8); ClientHTTP1 client("127.0.0.1", silent.port); client.timeout = 300ms; const auto start = std::chrono::steady_clock::now(); bool failed = false; try { client.Send(CreateRequestHTTP("GET", "/", "127.0.0.1")); } catch (const std::exception&) { failed = true; } const auto waited = std::chrono::steady_clock::now() - start; Check(failed, "a server that accepts and never answers fails the request"); Check(waited >= 280ms && waited < 5s, std::format("after about `timeout` ({} ms)", Milliseconds(waited))); } { // Connects queue up until the accept queue is full, which with a // backlog of 0 is after one or two; the next one goes unanswered. LoopbackListener full(0); std::vector queued; bool queueFull = false; for (int i = 0; i < 8 && !queueFull; ++i) { try { queued.emplace_back("127.0.0.1", full.port, 200ms); } catch (const SocketTimeoutException&) { queueFull = true; } } Check(queueFull, std::format("the accept queue fills up (after {} connects)", queued.size())); ClientHTTP1 client("127.0.0.1", full.port); client.connectTimeout = 300ms; const auto start = std::chrono::steady_clock::now(); bool timedOut = false; try { client.Send(CreateRequestHTTP("GET", "/", "127.0.0.1")); } catch (const SocketTimeoutException&) { timedOut = true; } catch (const std::exception&) { } const auto waited = std::chrono::steady_clock::now() - start; Check(timedOut, "a connect nobody answers throws SocketTimeoutException"); Check(waited >= 280ms && waited < 5s, std::format("after about `connectTimeout` ({} ms)", Milliseconds(waited))); } std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }