Use resolver order for connect_tcp Happy Eyeballs attempts - #1282
Conversation
connect_tcp() reordered the resolver results to force IPv6 first, even on hosts whose only IPv6 addresses are link-local or loopback, causing needless delays and failures when connecting to dual-stack services. Stop reordering and try the addresses in the order returned by the resolver, which follows RFC 6724 destination address selection: IPv6 is preferred when the host has a usable IPv6 connection, and IPv4 comes first otherwise. This matches how CPython, curl and Go handle Happy Eyeballs, and is what RFC 8305 section 4 prescribes. Fixes agronholm#1230 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
|
I guess the AI agent didn't bother reading AGENTS.md? |
|
If the system resolver can be relied on to prefer IPv6 addresses only when there's IPv6 connectivity, then this would be ideal, but that's a big "if". |
|
Digging deeper, the Happy Eyeballs v2 algorithm calls for interleaving the results by address family. Shouldn't we do that here? |
|
Fair questions on both counts. On AGENTS.md: that was an oversight on my part, sorry. On interleaving: you're right that RFC 8305 §5-style interleaving across address families is the fuller behavior; the current patch only orders the resolution results as the resolver returned them and doesn't interleave, so it does not yet satisfy what you describe. I don't want to hand-wave a quick fix — happy to take another pass at proper family-interleaved connection attempts with tests if you'd still want it here, otherwise feel free to close this draft. |
|
Restore the PR template and then implement the interleaving from RFC 8305. Then I'm willing to go forward with this. The original concern about address ordering is valid. Be sure to amend the documentation to indicate that we implement RFC 8305 (or more to the point, Happy Eyeballs v2) in any relevant places, assuming we're not missing anything else from the spec. |
Fixes #1230
Problem
connect_tcp()reorders the resolver results to force an IPv6 address first even when the host has no usable IPv6 connection (e.g. only link-local or loopback IPv6). On such hosts the first Happy Eyeballs attempt fails immediately with "Network is unreachable", wasting thehappy_eyeballs_delay(0.25s) before IPv4 is tried — and in some cases failing outright.Approach
Stop reordering, and try the addresses in the order returned by the resolver.
The system resolver implements RFC 6724 destination address selection, which already prefers IPv6 when the host has a usable IPv6 connection and returns IPv4 first (or omits IPv6 entirely) when it does not — exactly the signal the hardcoded reorder was trying to guess, but computed by the OS from the actual routing table.
This is also what RFC 8305 (Happy Eyeballs v2) section 4 prescribes: "the client SHOULD sort the IP addresses in the candidate set using the Destination Address Selection algorithm defined in [RFC6724]". It matches how CPython (
asyncio), curl and Go handle address ordering for connection attempts.In particular, on the reporter's system
getaddrinfoalready returned IPv4 before IPv6 — the reorder was undoing the OS's correct decision. On hosts with working IPv6, resolvers keep returning IPv6 first, so the IPv6 fast path is preserved.I deliberately did not attempt to detect IPv6 usability ourselves (e.g. by connecting to a well-known address or probing interfaces): that was the approach rejected in #1247, and the resolver already encodes the answer.
Testing
test_happy_eyeballsto assert the connection follows the resolver order when the resolver returns IPv4 first (the dual-stack server now sees the IPv4-mapped client,::ffff:127.0.0.1; the IPv6-only case still exercises the Happy Eyeballs fallback to::1).test_happy_eyeballs_prefers_ipv6_in_resolver_order, asserting that when the resolver returns IPv6 first the connection goes via::1.tests/test_sockets.py: 503 passed.ruffandmypyclean on the changed files.Changelog
Entry added to
docs/versionhistory.rstunder UNRELEASED.