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

Reply via email to