//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // A route table whose paths cannot be enumerated up front — `/order/`, // `/shop/` — is served through the listeners' `fallback` handler. The // same table is registered with ListenerHTTP1 and ListenerHTTP and asked the // same questions, because the point of the feature is that a URL means the // same thing over either protocol. import Crafter.Network; import Crafter.Thread; import std; using namespace Crafter; namespace { constexpr std::uint16_t kPortHTTP1 = 8096; constexpr std::uint16_t kPortHTTP1Plain = 8097; constexpr std::uint16_t kPortHTTP3 = 8098; int failures = 0; void Check(bool condition, std::string_view what) { if (!condition) { std::println("FAIL: {}", what); ++failures; } } // Every exchange the test performs, phrased so it can be replayed against // either protocol. `body` is matched exactly; an empty expectation means // "don't care". struct Exchange { std::string_view what; std::string_view path; std::string_view status; std::string_view body; }; constexpr std::array kExchanges = {{ // A registered route still wins: fallback only sees what routes miss. {"an exact route beats the fallback", "/", "200", "root"}, {"query strings still route to the path", "/?utm=1", "200", "root"}, // ...and everything else reaches the fallback with the target intact. {"an unknown path reaches the fallback", "/shop/blue-mug", "200", "slug:blue-mug"}, {"a second segment value reaches it too", "/shop/red-mug", "200", "slug:red-mug"}, {"the fallback sees the full target", "/shop/mug?ref=x", "200", "slug:mug?ref=x"}, {"the fallback may answer 404 itself", "/shop/", "404", "no such product"}, {"the fallback may answer non-404", "/order/deadbeef", "303", ""}, // A throwing fallback must be contained exactly like a throwing route. {"a throwing fallback becomes a 500", "/boom", "500", ""}, }}; std::unordered_map> MakeRoutes() { return { {"/", [](const HTTPRequest&) { return CreateResponseHTTP("200", "root"); }}, }; } // Stands in for an application router: the set of valid slugs and order // tokens is only known at runtime, so none of these paths could have been // registered in `routes`. HTTPResponse Fallback(const HTTPRequest& request) { const std::string_view path = PathWithoutQueryHTTP(request.path); if (path == "/boom") throw std::runtime_error("fallback exploded"); if (path.starts_with("/order/")) { return CreateResponseHTTP("303", {{"location", "/"}}, ""); } if (path.starts_with("/shop/")) { if (path.size() == std::string_view("/shop/").size()) { return CreateResponseHTTP("404", "no such product"); } // The full target, query string and all, reached the handler. return CreateResponseHTTP("200", std::format("slug:{}", request.path.substr(std::string_view("/shop/").size()))); } return CreateResponseHTTP("404", "unrouted"); } void Replay(std::string_view protocol, const std::function& send) { for (const Exchange& exchange : kExchanges) { HTTPResponse response = send(exchange.path); Check(response.status == exchange.status, std::format("{}: {} (status {}, wanted {})", protocol, exchange.what, response.status, exchange.status)); if (!exchange.body.empty()) { Check(response.body == exchange.body, std::format("{}: {} (body '{}', wanted '{}')", protocol, exchange.what, response.body, exchange.body)); } } } } int main() { ThreadPool::Start(); // A hung read would otherwise stall the whole suite. std::thread watchdog([] { std::this_thread::sleep_for(std::chrono::seconds(30)); std::println("timed out"); std::cout.flush(); std::_Exit(1); }); watchdog.detach(); try { // ── HTTP/1.1 over TCP ──────────────────────────────────────────── { ListenerAsyncHTTP1 listener(kPortHTTP1, MakeRoutes(), Fallback); ClientHTTP1 client("localhost", kPortHTTP1); Replay("http1", [&](std::string_view path) { return client.Send(CreateRequestHTTP("GET", std::string(path), "localhost")); }); // Without a fallback the listener keeps synthesising its own 404, // so nothing about the default behaviour moved. ListenerAsyncHTTP1 plain(kPortHTTP1Plain, MakeRoutes()); ClientHTTP1 plainClient("localhost", kPortHTTP1Plain); HTTPResponse missing = plainClient.Send( CreateRequestHTTP("GET", "/shop/blue-mug", "localhost")); Check(missing.status == "404", "http1: no fallback still means a synthetic 404"); Check(missing.body == "Not Found", "http1: ...with the listener's own body"); plain.Stop(); listener.Stop(); } // ── HTTP/3 over QUIC, same routes and same fallback ────────────── { QUICServerCredentials serverCreds; serverCreds.selfSigned = true; ListenerAsyncHTTP listener(kPortHTTP3, serverCreds, MakeRoutes(), Fallback); QUICClientCredentials clientCreds; clientCreds.insecureNoServerValidation = true; ClientHTTP client("localhost", kPortHTTP3, clientCreds); Replay("http3", [&](std::string_view path) { return client.Send(CreateRequestHTTP("GET", std::string(path), "localhost")); }); } } catch (const std::exception& error) { std::println("threw: {}", error.what()); std::cout.flush(); std::_Exit(1); } if (failures != 0) { std::println("{} check(s) failed", failures); std::cout.flush(); std::_Exit(1); } // See ShouldSendRecieveQUICStream: msquic's RegistrationClose blocks on // outstanding connections, so skip graceful teardown once we are done. std::cout.flush(); std::_Exit(0); }