[Interest] HTTP/2 on by default and "Happy Eyeballs" style connections in QNAM

Mårten Nordheim marten.nordheim at qt.io
Wed Aug 12 11:27:29 CEST 2026


Hello!

I have not seen this myself but for some servers I imagine it is very possibly,
but there has been an effort to fix the issue with stalls for happy eyeballs:
https://codereview.qt-project.org/c/qt/qtbase/+/679505

Since I just recently came back from vacation I haven't had time to look at it again.

As far as I remember, what is preferred depends on what is returned first in the
DNS query, so if the AAAA listings are first then we try IPv6 first currently.

Mårten:)

________________________________________
From: Interest <interest-bounces at qt-project.org> on behalf of Jakub Narolewski <izowiuz at gmail.com>
Sent: Tuesday, August 11, 2026 19:03
To: interest at qt-project.org
Subject: [Interest] HTTP/2 on by default and "Happy Eyeballs" style connections in QNAM

Hi,

I think I may have run into a problem in QNetworkAccessManager, but I'm
not sure yet whether
it's a bug, intended behaviour, or me misreading the sources.

The starting point was a host that takes over a minute to answer a plain
GET through QNAM,
while a browser on the same machine reaches it in a fraction of a
second. Reading the sources,
this is the chain I think I'm looking at.

```cpp
// qhttpnetworkrequest.cpp —
QHttpNetworkRequestPrivate::QHttpNetworkRequestPrivate

// v5.15.2:
       autoDecompress(false), pipeliningAllowed(false),
spdyAllowed(false), http2Allowed(false),

// 6.12.0-beta1:
       autoDecompress(false), pipeliningAllowed(false), http2Allowed(true),
```

https://github.com/qt/qtbase/blob/v5.15.2/src/network/access/qhttpnetworkrequest.cpp#L48
https://github.com/qt/qtbase/blob/v6.12.0-beta1/src/network/access/qhttpnetworkrequest.cpp#L17

An HTTP/2 connection gets exactly one active channel:

```cpp
// qhttpnetworkconnection.cpp
static int
getPreferredActiveChannelCount(QHttpNetworkConnection::ConnectionType type,
                                           int defaultValue)
{
     return (type == QHttpNetworkConnection::ConnectionTypeHTTP2
             || type == QHttpNetworkConnection::ConnectionTypeHTTP2Direct)
             ? 1
             : defaultValue;
}
```

https://github.com/qt/qtbase/blob/v6.12.0-beta1/src/network/access/qhttpnetworkconnection.cpp#L44-L51

And the same counter decides whether both address families are tried:

```cpp
// qhttpnetworkconnection.cpp

// This will be used if the host lookup found both and Ipv4 and
// Ipv6 address. Then we will start up two connections and pick
// the network layer of the one that finish first. The second
// connection will then be disconnected.
void QHttpNetworkConnectionPrivate::startNetworkLayerStateLookup()
{
     if (activeChannelCount > 1) {
         // At this time all channels should be unconnected.
         Q_ASSERT(!channels[0].isSocketBusy());
         Q_ASSERT(!channels[1].isSocketBusy());

         networkLayerState = IPv4or6;

         channels[0].networkLayerPreference = QAbstractSocket::IPv4Protocol;
         channels[1].networkLayerPreference = QAbstractSocket::IPv6Protocol;

         int timeout = 300;
         delayedConnectionTimer.start(timeout);
         if (delayIpv4)
             channels[1].ensureConnection();
         else
             channels[0].ensureConnection();
     } else {
         networkLayerState = IPv4or6;
         channels[0].networkLayerPreference =
QAbstractSocket::AnyIPProtocol;
         channels[0].ensureConnection();
     }
}
```

https://github.com/qt/qtbase/blob/v6.12.0-beta1/src/network/access/qhttpnetworkconnection.cpp#L1335-L1362


That single socket then walks the resolver's address list in order and
twice:

```cpp
// qabstractsocket.cpp, _q_startConnecting()
// Try all addresses twice.
addresses += addresses;
```

If I'm reading it right, then with HTTP/2 on we get no Happy Eyeballs
style connections at all,
and the timeout of the dead path becomes the cost of the request.

That would match what I measured. The host resolves to both A and AAAA
records, and the
IPv6 path is blackholed somewhat (it is hard to properly check this as
this is not happening on my machine).

[Warning! A little bit of hand waving as this is mostly postmortem.]

Sequentially that's ~21 s per dead address across four
addresses, so ~84 s before the A record is tried, at which point the
request finishes in ~120 ms. Since `transferTimeout` starts at request
time and only resets
when bytes arrive, any timeout below ~85 s just fails.

Also, is it possible to end up with ipv6 addresses in front of ipv4 in
per system-provided list? (As in, what Qt uses to try all the records).

Code quoted from 6.12.0-beta1; 6.7.2 looks the same to me.

There is an old report that reads like the same [?] thing: QTBUG-82151,
"QNetworkAccessManager
extermely slow on first call of get() with HTTP2" (5.14.1, 2020), closed
as not reproducible.
Looks like same'ish stuff over a minute on the first call, later calls
fine, disabling the HTTP/2
attribute makes it go away. The "later calls are fine" part would fit a
connection that gets
established once on the address that does answer and is then reused.

So, my questions are:

1. Am I reading this correctly?

2. Has anyone else hit this? The shape to look for is a request that
takes roughly a minute (at least on Windows)
    against a host with several AAAA records where the IPv6 path doesn't
answer, and that is
    instant again with HTTP/2 turned off (this last part is not yet
tested by me in my case).

3. Is turning HTTP/2 off per request the accepted way to deal with this,
or is there a
    supported way to keep HTTP/2 and still get the address-family
fallback? Turning it off
    everywhere feels like a large hammer for what may be a
connection-phase problem, but I
    haven't found anything better.

4. Anyone know good apple pie recipes?


--
Best regards / Pozdrawiam serdecznie
Narolewski Jakub
Software Developer

_______________________________________________
Interest mailing list
Interest at qt-project.org
https://lists.qt-project.org/listinfo/interest


More information about the Interest mailing list