//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // HTTP/1.1 connection reuse. Unlike HTTP/3 — where multiplexing is the // transport's job — keep-alive here means the same socket carries request // after request, and both ends have to agree on where each message ends. // This test pins that down: repeated requests must not open new // connections, `Connection: close` must end the connection, and the client // must recover when the server closes a pooled connection under it. import Crafter.Network; import Crafter.Thread; import std; using namespace Crafter; namespace { int failures = 0; void Check(bool condition, std::string_view what) { if (!condition) { std::println("FAIL: {}", what); ++failures; } } } int main() { ThreadPool::Start(); std::unordered_map> routes; routes["/"] = [](const HTTPRequest&) { return CreateResponseHTTP("200", "Hello World!"); }; routes["/once"] = [](const HTTPRequest&) { // A handler that asks for the connection to end after this reply. return CreateResponseHTTP("200", {{"connection", "close"}}, "bye"); }; try { ListenerAsyncHTTP1 listener(8091, std::move(routes)); ClientHTTP1 client("localhost", 8091); for (int i = 0; i < 5; ++i) { HTTPResponse response = client.Send(CreateRequestHTTP("GET", "/", "localhost")); Check(response.status == "200", "keep-alive request status"); Check(response.body == "Hello World!", "keep-alive request body"); Check(client.Connected(), "the client keeps the socket between requests"); } Check(listener.listener.AcceptedCount() == 1, "five requests shared one connection"); // A handler that answers with `connection: close` ends the // connection after its response; the client must notice and dial // again for the next request instead of writing into a dead socket. HTTPResponse closing = client.Send(CreateRequestHTTP("GET", "/once", "localhost")); Check(closing.status == "200", "close-flagged response still arrives"); Check(closing.body == "bye", "close-flagged response body"); Check(!client.Connected(), "the client drops a connection the server closed"); HTTPResponse after = client.Send(CreateRequestHTTP("GET", "/", "localhost")); Check(after.body == "Hello World!", "the next request redials transparently"); Check(listener.listener.AcceptedCount() == 2, "exactly one extra connection was opened"); // Force the stale-connection race that HTTP/1.1 keep-alive cannot // avoid: the server goes away while a pooled connection looks // usable. The client is expected to replay the request on a fresh // connection rather than surface the error. Check(client.Connected(), "a connection is pooled before the restart"); listener.Stop(); ListenerAsyncHTTP1 restarted(8091, {{"/", [](const HTTPRequest&) { return CreateResponseHTTP("200", "Hello Again!"); }}}); HTTPResponse recovered = client.Send(CreateRequestHTTP("GET", "/", "localhost")); Check(recovered.body == "Hello Again!", "the client recovers from a stale pooled connection"); Check(restarted.listener.AcceptedCount() == 1, "recovery dialled the new listener"); restarted.Stop(); } catch (const std::exception& error) { std::println("threw: {}", error.what()); ThreadPool::Stop(); return 1; } ThreadPool::Stop(); if (failures != 0) { std::println("{} check(s) failed", failures); return 1; } return 0; }