/* Crafter® Build Copyright (C) 2026 Catcrafts® Catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ // Regression test for the dropped-early-stream race in ListenerHTTP/ListenerQUIC. // // The server's connection callback (which receives PEER_STREAM_STARTED) is // installed in the listener's NEW_CONNECTION handler, but the per-connection // onConnect setup (which installs the OnStream handler + sends the server's // SETTINGS) is dispatched to the Crafter ThreadPool. An HTTP/3 peer opens its // control + request streams the instant the QUIC handshake completes — i.e. // potentially BEFORE onConnect has run. // // The bug: the original code installed only a no-op bootstrap connection // callback in NEW_CONNECTION and deferred the *real* callback to the same // ThreadPool task as onConnect. Any PEER_STREAM_STARTED that arrived before // that task ran was delivered to the bootstrap and silently dropped — the // stream was never adopted, so the request was never answered. With Chromium // (which opens streams aggressively right after the handshake) this surfaced // as an intermittent "WebTransport connection rejected" that cleared on retry. // // This test forces that window deterministically: it SATURATES the ThreadPool // before connecting, so onConnect cannot run until we release it. Meanwhile the // client opens its request stream immediately. The connection callback runs on // the msquic thread (not the ThreadPool), so with the fix it still buffers the // early stream; on the buggy build the bootstrap drops it. After we release the // pool, onConnect drains the buffer and the request is answered — or, on the // buggy build, the request stream is gone and the read below hangs until the // watchdog fails the test. import Crafter.Network; import Crafter.Thread; import std; using namespace Crafter; namespace { // Read one HTTP/3 frame off `stream` → (frameType, payload). Unconsumed // tail bytes are PrependReceived'd back. (Same helper shape as the // ShouldEchoWebTransport test.) std::pair> ReadFrame(QUICStream& stream) { std::vector buf; std::uint64_t type = 0; std::size_t cn = 0; while (true) { const auto* p = reinterpret_cast(buf.data()); if (HTTP3::DecodeVarint(p, buf.size(), type, cn)) break; auto chunk = stream.RecieveSync(); buf.insert(buf.end(), chunk.begin(), chunk.end()); } buf = std::vector(buf.begin() + cn, buf.end()); std::uint64_t len = 0; std::size_t lc = 0; while (true) { const auto* p = reinterpret_cast(buf.data()); if (HTTP3::DecodeVarint(p, buf.size(), len, lc)) break; auto chunk = stream.RecieveSync(); buf.insert(buf.end(), chunk.begin(), chunk.end()); } buf = std::vector(buf.begin() + lc, buf.end()); while (buf.size() < len) { auto chunk = stream.RecieveSync(); buf.insert(buf.end(), chunk.begin(), chunk.end()); } std::vector payload(buf.begin(), buf.begin() + len); std::vector tail(buf.begin() + len, buf.end()); if (!tail.empty()) stream.PrependReceived(std::move(tail)); return {type, std::move(payload)}; } } int main() { ThreadPool::Start(); constexpr std::uint16_t kPort = 8086; // Watchdog: on the buggy build the request stream is dropped and the // response read below blocks forever. Fail the test instead of hanging // the whole suite. std::thread watchdog([] { std::this_thread::sleep_for(std::chrono::seconds(8)); std::println("timed out waiting for response — early request stream was dropped"); std::cout.flush(); std::_Exit(1); }); watchdog.detach(); // ── Server: a single HTTP route. ───────────────────────────────────── QUICServerCredentials serverCreds; serverCreds.selfSigned = true; std::unordered_map> routes = { {"/", [](const HTTPRequest&) { return CreateResponseHTTP("200", "ok"); }}, }; ListenerAsyncHTTP listener(kPort, serverCreds, std::move(routes)); // ── Saturate the ThreadPool so the server's onConnect can't run yet. ── // The connection callback (PEER_STREAM_STARTED) runs on the msquic thread // and is unaffected; only onConnect (OnStream install + SETTINGS) is gated. std::atomic release{false}; std::atomic parked{0}; for (int i = 0; i < 256; ++i) { ThreadPool::Enqueue([&] { parked.fetch_add(1); while (!release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); }); } // Give the pool a moment to actually pick up and block on those tasks. std::this_thread::sleep_for(std::chrono::milliseconds(100)); try { // ── Client: open the request stream immediately after the handshake, // i.e. while onConnect is still gated behind the saturated pool. ──── QUICClientCredentials clientCreds; clientCreds.insecureNoServerValidation = true; ClientQUIC quic("localhost", kPort, std::string(HTTP3::kAlpn), clientCreds); // Drain peer-initiated (server) unidi streams once they appear. quic.OnStream([](QUICStream stream) { try { while (true) (void)stream.RecieveSync(); } catch (...) {} }); // Client control stream + SETTINGS. QUICStream control = quic.OpenStream(/*unidirectional=*/true); auto prelude = HTTP3::BuildControlStreamPrelude(); control.SendSync(prelude.data(), static_cast(prelude.size()), /*finish=*/false); // GET / on a fresh bidi request stream, FIN immediately. QUICStream req = quic.OpenStream(/*unidirectional=*/false); std::vector> fields = { {":method", "GET"}, {":scheme", "https"}, {":authority", "localhost"}, {":path", "/"}, }; auto headerPayload = HTTP3::EncodeFieldSection(fields); std::vector wire; HTTP3::WriteFrame(wire, HTTP3::kFrameHeaders, headerPayload.data(), headerPayload.size()); req.SendSync(wire.data(), static_cast(wire.size()), /*finish=*/true); // Let the early streams reach the server and be buffered (fix) or // dropped (bug) while onConnect is still gated. std::this_thread::sleep_for(std::chrono::milliseconds(300)); // Now release the pool — onConnect runs, and with the fix it drains the // buffered request stream and answers it. release.store(true); // Read the response HEADERS frame and check the status. auto [respType, respPayload] = ReadFrame(req); if (respType != HTTP3::kFrameHeaders) { std::println("expected HEADERS response frame, got type {}", respType); return 1; } auto respFields = HTTP3::DecodeFieldSection( reinterpret_cast(respPayload.data()), respPayload.size()); std::string status; for (auto& [k, v] : respFields) if (k == ":status") status = v; if (status != "200") { std::println("expected status 200, got '{}'", status); return 1; } std::_Exit(0); } catch (std::exception& e) { release.store(true); std::println("client failed: {}", e.what()); return 1; } }