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 <[email protected]> on behalf of Jakub Narolewski <[email protected]> Sent: Tuesday, August 11, 2026 19:03 To: [email protected] 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 [email protected] https://lists.qt-project.org/listinfo/interest _______________________________________________ Interest mailing list [email protected] https://lists.qt-project.org/listinfo/interest
