//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Canonical HTTP/1.1 client/server round-trip: the same shape as // ShouldSendRecieveHTTP, over TCP instead of QUIC. import Crafter.Network; 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() { std::unordered_map> routes; routes["/"] = [](const HTTPRequest&) { return CreateResponseHTTP("200", "Hello World!"); }; routes["/echo"] = [](const HTTPRequest& request) { return CreateResponseHTTP("200", {{"content-type", "text/plain"}}, request.body); }; routes["/query"] = [](const HTTPRequest& request) { return CreateResponseHTTP("200", request.path); }; routes["/boom"] = [](const HTTPRequest&) -> HTTPResponse { throw std::runtime_error("handler exploded"); }; try { ListenerAsyncHTTP1 listener(8090, std::move(routes)); ClientHTTP1 client("localhost", 8090); HTTPResponse hello = client.Send(CreateRequestHTTP("GET", "/", "localhost")); Check(hello.status == "200", "GET / status"); Check(hello.body == "Hello World!", "GET / body"); Check(hello.headers.contains("date"), "the server stamps a Date header"); Check(hello.headers.at("content-length") == "12", "content-length matches the body"); HTTPResponse echoed = client.Send(CreateRequestHTTP("POST", "/echo", "localhost", std::string("ping pong"))); Check(echoed.status == "200", "POST /echo status"); Check(echoed.body == "ping pong", "POST /echo returns the request body"); Check(echoed.headers.at("content-type") == "text/plain", "handler headers survive"); // Query strings must reach the handler intact while still routing on // the bare path — browsers append them to everything. HTTPResponse query = client.Send(CreateRequestHTTP("GET", "/query?a=1&b=2", "localhost")); Check(query.status == "200", "query-string request routes to the bare path"); Check(query.body == "/query?a=1&b=2", "the handler sees the full target"); HTTPResponse missing = client.Send(CreateRequestHTTP("GET", "/nope", "localhost")); Check(missing.status == "404", "unknown route is a 404"); // A HEAD gets the headers a GET would produce, and no body. HTTPResponse head = client.Send(CreateRequestHTTP("HEAD", "/", "localhost")); Check(head.status == "200", "HEAD status"); Check(head.body.empty(), "HEAD has no body"); Check(head.headers.at("content-length") == "12", "HEAD still advertises the length"); // A throwing handler must become a 500, not a dropped connection. HTTPResponse boom = client.Send(CreateRequestHTTP("GET", "/boom", "localhost")); Check(boom.status == "500", "a throwing handler yields 500"); Check(boom.body.find("handler exploded") != std::string::npos, "500 carries the reason"); // Every exchange above shared one connection. Check(listener.listener.AcceptedCount() == 1, "the whole test used a single connection"); listener.Stop(); } catch (const std::exception& error) { std::println("threw: {}", error.what()); return 1; } if (failures != 0) { std::println("{} check(s) failed", failures); return 1; } return 0; }