//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // A QUIC stream's async calls report a closed stream to their callbacks, the // same way on native as in the browser build, which has no exceptions: // - a receive on a closed stream gets an empty chunk. Natively it used to // let QUICClosedException escape the ThreadPool task, which ended the // program; // - RecieveUntilFullAsync on a stream that closes early gets what arrived; // RecieveUntilFullSync still throws, and leaves those bytes for the next // receive instead of dropping them; // - SendAsync tells its callback whether the stream took the buffer. It // used to call it the same way either way. // A server answers a stream with "abc" and closes its side. #include import Crafter.Network; import Crafter.Thread; import std; using namespace Crafter; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } // A callback's result, shared with it so it outlives whichever thread sets it. template struct Outcome { std::mutex mutex; std::condition_variable done; std::optional value; void Set(T result) { { std::lock_guard lock(mutex); value = std::move(result); } done.notify_all(); } std::optional Wait() { std::unique_lock lock(mutex); done.wait_for(lock, std::chrono::seconds(5), [&] { return value.has_value(); }); return value; } }; std::string Text(const std::vector& bytes) { return std::string(bytes.begin(), bytes.end()); } } // namespace int main() { ThreadPool::Start(); constexpr std::uint16_t port = 9188; constexpr std::string_view alpn = "crafter/test-closed-stream"; QUICServerCredentials serverCreds; serverCreds.selfSigned = true; ListenerQUIC listener(port, std::string(alpn), serverCreds, [&](ClientQUIC* peer) { peer->OnStream([](QUICStream stream) { try { (void)stream.RecieveSync(); stream.SendSync("abc", 3, true); } catch (...) {} }); }); listener.ListenAsyncAsync(); try { // ── A stream that was never opened ─────────────────────────────────── { QUICStream closed; auto received = std::make_shared>(); closed.RecieveAsync([received](std::vector chunk) { received->Set(Text(chunk)); }); const std::optional got = received->Wait(); Check(got && got->empty(), "a receive on a closed stream gets an empty chunk, and the program carries on"); auto sent = std::make_shared>(); closed.SendAsync("x", 1, false, [sent](bool ok) { sent->Set(ok); }); const std::optional ok = sent->Wait(); Check(ok && !*ok, "a send on it tells its callback the stream didn't take it"); } QUICClientCredentials clientCreds; clientCreds.insecureNoServerValidation = true; ClientQUIC client("localhost", port, std::string(alpn), clientCreds); // ── The server answers, then closes its side ───────────────────────── { QUICStream stream = client.OpenStream(); auto sent = std::make_shared>(); stream.SendAsync("hi", 2, false, [sent](bool ok) { sent->Set(ok); }); const std::optional ok = sent->Wait(); Check(ok && *ok, "a send the stream takes tells its callback so"); auto first = std::make_shared>(); stream.RecieveUntilFullAsync(10, [first](std::vector chunk) { first->Set(Text(chunk)); }); const std::optional got = first->Wait(); Check(got == "abc", std::format("RecieveUntilFullAsync on a stream that closes early gets what arrived ({})", got.value_or("nothing"))); auto after = std::make_shared>(); stream.RecieveAsync([after](std::vector chunk) { after->Set(Text(chunk)); }); const std::optional next = after->Wait(); Check(next && next->empty(), "a receive after the close gets an empty chunk"); } { QUICStream stream = client.OpenStream(); stream.SendSync("hi", 2, false); bool threw = false; try { (void)stream.RecieveUntilFullSync(10); } catch (const QUICClosedException&) { threw = true; } const std::vector left = stream.RecieveUntilCloseSync(); Check(threw && Text(left) == "abc", std::format("RecieveUntilFullSync throws on the early close and leaves the bytes for the next receive ({})", Text(left))); } std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); // As the other QUIC tests: skip the static teardown, which waits for // the accepted peer's idle timeout. _Exit doesn't flush. std::fflush(stdout); std::_Exit(failures == 0 ? 0 : 1); } catch (const std::exception& e) { std::println("FAIL {}", e.what()); std::fflush(stdout); std::_Exit(1); } }