89 lines
3.7 KiB
C++
89 lines
3.7 KiB
C++
|
|
//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<std::string, std::function<HTTPResponse(const HTTPRequest&)>> 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;
|
||
|
|
}
|