From 2971d86c22db291a5017baeed6f8c7cdc05816ca Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Fri, 18 Sep 2026 13:03:13 -0500 Subject: [PATCH] test: use a never-listened port for the refused-connection fixtures The http transport tests opened an acceptor on an ephemeral loopback port, closed it, and connected expecting ECONNREFUSED. Listening is what breaks that: on a host whose listener teardown is asynchronous -- WSL2 mirrored networking among them -- the port keeps accepting for several milliseconds after close and then resets, which the transport correctly classifies as an io failure rather than a connect failure. Bind a plain socket to obtain a free port, never listen on it, and release it. The port is unconnectable for the same reason it was before, without ever having entered the listening state that opens that window. Holding a bound-but-unlistening socket open instead would also close the local port-reuse race, but a bound-unlistening socket does not yield a connect failure on macOS, so it is not portable. stale_metadata_reconnect_failure_cleans_up_safely is unchanged: it needs a real server for its first request, so its port genuinely listens and no choice of port helps it. --- .../libfc/test/network/test_http_client.cpp | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/libraries/libfc/test/network/test_http_client.cpp b/libraries/libfc/test/network/test_http_client.cpp index ba194cb257..b4b8607e4c 100644 --- a/libraries/libfc/test/network/test_http_client.cpp +++ b/libraries/libfc/test/network/test_http_client.cpp @@ -424,6 +424,26 @@ fc::http_file_download_options download_options(uint64_t max_body_bytes) { }; } +/** + * A loopback port that refuses connections. + * + * Bound briefly so the kernel hands out a port nothing else holds, never listened on, then + * released. The socket must never listen: a port that has listened keeps accepting for a short + * window after it closes on hosts whose listener teardown is asynchronous -- WSL2 mirrored + * networking among them -- and the connection is then reset, which the transport correctly + * reports as an io failure rather than a connect failure. Holding the socket open instead of + * closing it does not work either: a bound-but-unlistening socket does not produce a connect + * failure on macOS. + */ +uint16_t unconnectable_loopback_port() { + boost::asio::io_context io; + tcp::socket probe(io, tcp::endpoint(boost::asio::ip::address_v4::loopback(), 0)); + const auto port = probe.local_endpoint().port(); + boost::system::error_code ec; + probe.close(ec); + return port; +} + /** Return the URL for @p server. */ fc::url server_url(const scripted_http_server& server) { return fc::url("http://127.0.0.1:" + std::to_string(server.port()) + "/download"); @@ -1329,11 +1349,7 @@ BOOST_AUTO_TEST_CASE(dns_resolver_start_failure_is_classified) { /// A successful injected DNS result is used for the bounded connection attempt. BOOST_AUTO_TEST_CASE(dns_resolution_accepts_completed_lookup) { - boost::asio::io_context io; - tcp::acceptor closed_listener(io, tcp::endpoint(boost::asio::ip::address_v4::loopback(), 0)); - const auto closed_port = closed_listener.local_endpoint().port(); - boost::system::error_code close_error; - closed_listener.close(close_error); + const auto refused_port = unconnectable_loopback_port(); std::atomic_uint32_t resolve_count{0}; auto resolver = [&](const std::string&, const std::string&, fc::time_point, fc::http::detail::resolver_complete_fn complete) { @@ -1341,7 +1357,7 @@ BOOST_AUTO_TEST_CASE(dns_resolution_accepts_completed_lookup) { complete(std::nullopt, { { .address = "127.0.0.1", - .port = closed_port, + .port = refused_port, } }); return [] {}; @@ -1362,11 +1378,7 @@ BOOST_AUTO_TEST_CASE(dns_resolution_accepts_completed_lookup) { /// DNS TTL and connection-failure refresh are independent cache policies. BOOST_AUTO_TEST_CASE(dns_cache_refresh_policy_is_preserved) { - boost::asio::io_context io; - tcp::acceptor closed_listener(io, tcp::endpoint(boost::asio::ip::address_v4::loopback(), 0)); - const auto closed_port = closed_listener.local_endpoint().port(); - boost::system::error_code close_error; - closed_listener.close(close_error); + const auto refused_port = unconnectable_loopback_port(); const auto exercise = [&](std::optional cache_timeout, bool refresh_on_connection_failure) { std::atomic_uint32_t resolve_count{0}; @@ -1380,7 +1392,7 @@ BOOST_AUTO_TEST_CASE(dns_cache_refresh_policy_is_preserved) { complete(std::nullopt, { { .address = "127.0.0.1", - .port = closed_port, + .port = refused_port, } }); return [] {}; @@ -1426,11 +1438,7 @@ BOOST_AUTO_TEST_CASE(retries_require_explicit_idempotency) { /// Exhausted retries produce a stable category without replaying more than the configured attempts. BOOST_AUTO_TEST_CASE(idempotent_retry_exhaustion_is_bounded) { - boost::asio::io_context io; - tcp::acceptor closed_listener(io, tcp::endpoint(boost::asio::ip::address_v4::loopback(), 0)); - const auto closed_port = closed_listener.local_endpoint().port(); - boost::system::error_code close_error; - closed_listener.close(close_error); + const auto refused_port = unconnectable_loopback_port(); fc::http::transport transport; auto options = tls_request_options(); @@ -1441,7 +1449,7 @@ BOOST_AUTO_TEST_CASE(idempotent_retry_exhaustion_is_bounded) { BOOST_CHECK_EXCEPTION(transport.perform( fc::http::request{ .method = fc::http::request_method::get, - .target = fc::url("http://127.0.0.1:" + std::to_string(closed_port) + "/"), + .target = fc::url("http://127.0.0.1:" + std::to_string(refused_port) + "/"), }, options), fc::exception, [](const fc::exception& error) { @@ -2047,18 +2055,14 @@ BOOST_AUTO_TEST_CASE(stale_metadata_reconnect_failure_cleans_up_safely) { /// The stale-connection flag does not retry a failure on the first fresh connection. BOOST_AUTO_TEST_CASE(fresh_download_connection_failure_is_not_retried) { - boost::asio::io_context io; - tcp::acceptor closed_listener(io, tcp::endpoint(boost::asio::ip::address_v4::loopback(), 0)); - const auto closed_port = closed_listener.local_endpoint().port(); - boost::system::error_code close_error; - closed_listener.close(close_error); + const auto refused_port = unconnectable_loopback_port(); fc::temp_directory temp; const auto output = temp.path() / "fresh-connect-failure.bin"; fc::http_client client; auto options = download_options(exact_body_bytes); options.retry_failed_reused_connection = true; - BOOST_CHECK_EXCEPTION(client.post_to_file(fc::url("http://127.0.0.1:" + std::to_string(closed_port) + "/download"), + BOOST_CHECK_EXCEPTION(client.post_to_file(fc::url("http://127.0.0.1:" + std::to_string(refused_port) + "/download"), fc::variant(fc::mutable_variant_object()), output, options), fc::exception, [](const fc::exception& error) { const auto detail = error.to_detail_string();