[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