Skip to content

Source download: no address-family fallback — a black-holed IPv6 path fails the download even though IPv4 works #325

Closed as not planned

Description

@DutchJaFO

Description

SocketsHttpHandler attempts the addresses returned for a host in OS-provided order and does not race
address families. On a machine where IPv6 is routed but unreachable — a default route exists, so SYNs are
sent and silently dropped rather than failing fast — every IPv6 address is tried in turn until the connect
budget expires. The working IPv4 addresses are never reached, and the source is reported unreachable
despite being perfectly reachable.

This is not the same defect as #323. #323 bounded how long a stalled attempt may run; this is about the
attempt targeting an address family that cannot work while a working one goes untried. #323's
ConnectTimeout remains a prerequisite — without it the parallel attempt introduced here would itself be
unbounded.

Home Assistant and homelab deployments routinely have partial or flapping IPv6, so this is not specific to
one developer machine.

Reproduction steps

Against raw.githubusercontent.com during a window when IPv6 is black-holed, using SocketsHttpHandler
with ConnectTimeout = 10s:

DNS order as .NET sees it:
    InterNetworkV6  2606:50c0:8002::154   <- tried first
    ... 3 more AAAA ...
    InterNetwork    185.199.111.133       <- never reached
    ... 3 more A ...

app default (dual-stack)   FAIL in 10.03s -> ConnectTimeout
forced IPv4                OK 200 in  0.49s
forced IPv6                FAIL in 10.00s -> ConnectTimeout

Same host, same moment: IPv4 works in half a second while the default path times out.

Note the condition is intermittent on the machine where it was found — IPv6 reachability comes and goes,
and a later run had all three variants succeeding. The defect is not that IPv6 is down; it is that the
download fails when it is, instead of using the family that works.

Expected behaviour

  1. A connect attempt races both address families rather than exhausting one before trying the other
    (RFC 8305 Happy Eyeballs: start the preferred family, start the other after a short delay, take the
    first to succeed, cancel the loser).
  2. A host reachable over either family connects promptly regardless of which family is degraded.
  3. The behaviour stays inside the ConnectTimeout budget Source download: a stalled connection attempt outlives its request and fails every other source on the same host #323 introduced.
  4. A single-family host (only A, or only AAAA) still connects normally.

Actual behaviour

The connect budget is spent entirely on the first-listed family. With ConnectTimeout = 10s the request
fails with:

System.TimeoutException: A connection could not be established within the configured ConnectTimeout.
   at System.Net.Http.HttpConnectionPool.CreateConnectTimeoutException(OperationCanceledException oce)
   at System.Net.Http.HttpConnectionPool.InjectNewHttp11ConnectionAsync(QueueItem queueItem)
   ...
   at System.Net.Sockets.SocketAsyncEventArgs.<DnsConnectAsync>g__Core|113_0(MultiConnectSocketAsyncEventArgs ...)

MultiConnectSocketAsyncEventArgs in that stack is the sequential multi-address walk: one family at a
time, in order, with no concurrent attempt on the other.

Failing tests

Test class Test method Status before fix
SourceCacheHttpClientTests SourceCacheClient_PrimaryHandler_HasHappyEyeballsConnectCallback ❌ red
HappyEyeballsConnectTests Connect_PreferredFamilyBlackHoled_FallsBackToWorkingFamilyPromptly ❌ red
HappyEyeballsConnectTests Connect_BothFamiliesReachable_UsesFirstToConnect guard (passed pre-fix)
HappyEyeballsConnectTests Connect_OnlyIPv4Resolved_ConnectsWithoutDelay guard (passed pre-fix)
HappyEyeballsConnectTests Connect_OnlyIPv6Resolved_ConnectsWithoutDelay guard (passed pre-fix)
HappyEyeballsConnectTests Connect_AllAddressesBlackHoled_FailsWithinConnectTimeout guard (passed pre-fix)

Only the first two demonstrate the defect. The algorithm tests cannot compile without the new type, so
they were first run against a deliberately naive implementation (a sequential walk in resolved order,
reproducing the SocketsHttpHandler behaviour being replaced) to make failure an assertion failure rather
than a compile error. Against that, the four marked "guard" passed — a sequential walk genuinely handles
single-family and both-reachable hosts correctly. They are regression guards, recorded as such rather than
described as red. See the plan doc's "Finding" section, including the two test defects this process
surfaced.

Definition of done

  • Failing test(s) listed above are red before the fix is written
  • Fix implemented
  • All listed tests pass (green)
  • No regression in related tests
  • Findings summarised in a closing comment

Activity

  1. added this to the Notification system milestone on Aug 18, 2026
  2. added 3 commits that reference this issue on Aug 18, 2026
  3. DutchJaFO commented on Aug 20, 2026

    @DutchJaFO
    OwnerAuthor

    This bug was the result of an overengineered solution to a problem.

  4. added 7 commits that reference this issue on Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions