Add a timeout for transfers

If a transfer timeout is set for QNetworkRequest, downloads
and uploads are aborted if the timeout expires and bytes
haven't been transmitted in either direction.

Task-number: QTBUG-3443
Change-Id: I702d223d673f0c6612343dc9d053815acfcb61b8
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
bb10
Ville Voutilainen 2019-10-17 17:38:53 +03:00
parent 5d4b5dab7f
commit c33d8bfc99
5 changed files with 154 additions and 5 deletions

View File

@ -461,6 +461,7 @@ QNetworkReplyHttpImplPrivate::QNetworkReplyHttpImplPrivate()
, preMigrationDownloaded(-1)
, bytesDownloaded(0)
, bytesBuffered(0)
, transferTimeout(nullptr)
, downloadBufferReadPosition(0)
, downloadBufferCurrentSize(0)
, downloadZerocopyBuffer(nullptr)
@ -1067,6 +1068,7 @@ void QNetworkReplyHttpImplPrivate::replyDownloadData(QByteArray d)
if (!isHttpRedirectResponse()) {
buffer.append(d);
bytesDownloaded += d.size();
setupTransferTimeout();
}
bytesBuffered += d.size();
@ -1401,6 +1403,7 @@ void QNetworkReplyHttpImplPrivate::replyDownloadProgressSlot(qint64 bytesReceive
return;
bytesDownloaded = bytesReceived;
setupTransferTimeout();
downloadBufferCurrentSize = bytesReceived;
@ -1857,7 +1860,6 @@ bool QNetworkReplyHttpImplPrivate::startWaitForSession(QSharedPointer<QNetworkSe
void QNetworkReplyHttpImplPrivate::_q_startOperation()
{
Q_Q(QNetworkReplyHttpImpl);
if (state == Working) // ensure this function is only being called once
return;
@ -1897,6 +1899,7 @@ void QNetworkReplyHttpImplPrivate::_q_startOperation()
}
#endif // QT_NO_BEARERMANAGEMENT
setupTransferTimeout();
if (synchronous) {
state = Finished;
q_func()->setFinished(true);
@ -2033,6 +2036,31 @@ void QNetworkReplyHttpImplPrivate::_q_bufferOutgoingData()
}
}
void QNetworkReplyHttpImplPrivate::_q_transferTimedOut()
{
Q_Q(QNetworkReplyHttpImpl);
q->abort();
}
void QNetworkReplyHttpImplPrivate::setupTransferTimeout()
{
Q_Q(QNetworkReplyHttpImpl);
if (!transferTimeout) {
transferTimeout = new QTimer(q);
QObject::connect(transferTimeout, SIGNAL(timeout()),
q, SLOT(_q_transferTimedOut()),
Qt::QueuedConnection);
}
transferTimeout->stop();
if (request.transferTimeout()) {
transferTimeout->setSingleShot(true);
transferTimeout->setInterval(request.transferTimeout());
QMetaObject::invokeMethod(transferTimeout, "start",
Qt::QueuedConnection);
}
}
#ifndef QT_NO_BEARERMANAGEMENT
void QNetworkReplyHttpImplPrivate::_q_networkSessionConnected()
{
@ -2115,6 +2143,8 @@ void QNetworkReplyHttpImplPrivate::emitReplyUploadProgress(qint64 bytesSent, qin
if (isFinished)
return;
setupTransferTimeout();
if (!emitAllUploadProgressSignals) {
//choke signal emissions, except the first and last signals which are unconditional
if (uploadProgressSignalChoke.isValid()) {
@ -2126,7 +2156,6 @@ void QNetworkReplyHttpImplPrivate::emitReplyUploadProgress(qint64 bytesSent, qin
uploadProgressSignalChoke.start();
}
}
emit q->uploadProgress(bytesSent, bytesTotal);
}
@ -2159,7 +2188,8 @@ void QNetworkReplyHttpImplPrivate::_q_finished()
void QNetworkReplyHttpImplPrivate::finished()
{
Q_Q(QNetworkReplyHttpImpl);
if (transferTimeout)
transferTimeout->stop();
if (state == Finished || state == Aborted || state == WaitingForSession)
return;

View File

@ -59,6 +59,7 @@
#include "QtCore/qdatetime.h"
#include "QtCore/qsharedpointer.h"
#include "QtCore/qscopedpointer.h"
#include "QtCore/qtimer.h"
#include "qatomic.h"
#include <QtNetwork/QNetworkCacheMetaData>
@ -100,6 +101,7 @@ public:
Q_PRIVATE_SLOT(d_func(), void _q_cacheLoadReadyRead())
Q_PRIVATE_SLOT(d_func(), void _q_bufferOutgoingData())
Q_PRIVATE_SLOT(d_func(), void _q_bufferOutgoingDataFinished())
Q_PRIVATE_SLOT(d_func(), void _q_transferTimedOut())
#ifndef QT_NO_BEARERMANAGEMENT
Q_PRIVATE_SLOT(d_func(), void _q_networkSessionConnected())
Q_PRIVATE_SLOT(d_func(), void _q_networkSessionFailed())
@ -181,6 +183,9 @@ public:
void _q_cacheSaveDeviceAboutToClose();
void _q_transferTimedOut();
void setupTransferTimeout();
#ifndef QT_NO_BEARERMANAGEMENT
void _q_networkSessionConnected();
void _q_networkSessionFailed();
@ -250,6 +255,8 @@ public:
qint64 bytesDownloaded;
qint64 bytesBuffered;
QTimer *transferTimeout;
// Only used when the "zero copy" style is used.
// Please note that the whole "zero copy" download buffer API is private right now. Do not use it.
qint64 downloadBufferReadPosition;

View File

@ -425,6 +425,18 @@ QT_BEGIN_NAMESPACE
based on some app-specific configuration.
*/
/*!
\enum QNetworkRequest::TransferTimeoutConstant
\since 5.15
A constant that can be used for enabling transfer
timeouts with a preset value.
\value TransferTimeoutPreset The transfer timeout in milliseconds.
Used if setTimeout() is called
without an argument.
*/
class QNetworkRequestPrivate: public QSharedData, public QNetworkHeadersPrivate
{
public:
@ -435,6 +447,7 @@ public:
, sslConfiguration(0)
#endif
, maxRedirectsAllowed(maxRedirectCount)
, transferTimeout(0)
{ qRegisterMetaType<QNetworkRequest>(); }
~QNetworkRequestPrivate()
{
@ -459,6 +472,7 @@ public:
#if QT_CONFIG(http)
h2Configuration = other.h2Configuration;
#endif
transferTimeout = other.transferTimeout;
}
inline bool operator==(const QNetworkRequestPrivate &other) const
@ -472,6 +486,7 @@ public:
#if QT_CONFIG(http)
&& h2Configuration == other.h2Configuration
#endif
&& transferTimeout == other.transferTimeout
;
// don't compare cookedHeaders
}
@ -486,6 +501,7 @@ public:
#if QT_CONFIG(http)
QHttp2Configuration h2Configuration;
#endif
int transferTimeout;
};
/*!
@ -909,6 +925,40 @@ void QNetworkRequest::setHttp2Configuration(const QHttp2Configuration &configura
{
d->h2Configuration = configuration;
}
/*!
\since 5.15
Returns the timeout used for transfers, in milliseconds.
This timeout is zero if setTransferTimeout hasn't been
called, which means that the timeout is not used.
\sa setTransferTimeout
*/
int QNetworkRequest::transferTimeout()
{
return d->transferTimeout;
}
/*!
\since 5.15
Sets \a timeout as the transfer timeout in milliseconds.
Transfers are aborted if no bytes are transferred before
the timeout expires. Zero means no timer is set. If no
argument is provided, the timeout is
QNetworkRequest::TransferTimeoutPreset. If this function
is not called, the timeout is disabled and has the
value zero.
\sa transferTimeout
*/
void QNetworkRequest::setTransferTimeout(int timeout)
{
d->transferTimeout = timeout;
}
#endif // QT_CONFIG(http) || defined(Q_CLANG_QDOC)
static QByteArray headerName(QNetworkRequest::KnownHeaders header)

View File

@ -134,6 +134,9 @@ public:
UserVerifiedRedirectPolicy
};
enum TransferTimeoutConstant {
TransferTimeoutPreset = 30000
};
QNetworkRequest();
explicit QNetworkRequest(const QUrl &url);
@ -185,6 +188,9 @@ public:
#if QT_CONFIG(http) || defined(Q_CLANG_QDOC)
QHttp2Configuration http2Configuration() const;
void setHttp2Configuration(const QHttp2Configuration &configuration);
int transferTimeout();
void setTransferTimeout(int timeout = TransferTimeoutPreset);
#endif // QT_CONFIG(http) || defined(Q_CLANG_QDOC)
private:
QSharedDataPointer<QNetworkRequestPrivate> d;

View File

@ -511,6 +511,8 @@ private Q_SLOTS:
void autoDeleteReplies_data();
void autoDeleteReplies();
void getWithTimeout();
void postWithTimeout();
// NOTE: This test must be last!
void parentingRepliesToTheApp();
private:
@ -589,6 +591,7 @@ public:
bool multiple;
int totalConnections;
bool stopTransfer = false;
bool hasContent = false;
int contentRead = 0;
int contentLength = 0;
@ -655,7 +658,7 @@ protected:
// we need to emulate the bytesWrittenSlot call if the data is empty.
if (dataToTransmit.size() == 0) {
emit client->bytesWritten(0);
} else {
} else if (!stopTransfer) {
client->write(dataToTransmit);
// FIXME: For SSL connections, if we don't flush the socket, the
// client never receives the data and since we're doing a disconnect
@ -711,7 +714,8 @@ public slots:
Q_ASSERT(currentClient);
if (currentClient != client)
client = currentClient;
if (stopTransfer)
return;
receivedData += client->readAll();
const int doubleEndlPos = receivedData.indexOf("\r\n\r\n");
@ -9342,6 +9346,58 @@ void tst_QNetworkReply::autoDeleteReplies()
}
}
void tst_QNetworkReply::getWithTimeout()
{
MiniHttpServer server(tst_QNetworkReply::httpEmpty200Response, false);
QNetworkRequest request(QUrl("http://localhost:" + QString::number(server.serverPort())));
QNetworkReplyPtr reply(manager.get(request));
QSignalSpy spy(reply.data(), SIGNAL(error(QNetworkReply::NetworkError)));
QCOMPARE(waitForFinish(reply), int(Success));
QCOMPARE(spy.count(), 0);
QVERIFY(reply->error() == QNetworkReply::NoError);
request.setTransferTimeout(1000);
server.stopTransfer = true;
QNetworkReplyPtr reply2(manager.get(request));
QSignalSpy spy2(reply2.data(), SIGNAL(error(QNetworkReply::NetworkError)));
QCOMPARE(waitForFinish(reply2), int(Failure));
QCOMPARE(spy2.count(), 1);
QVERIFY(reply2->error() == QNetworkReply::OperationCanceledError);
}
void tst_QNetworkReply::postWithTimeout()
{
MiniHttpServer server(tst_QNetworkReply::httpEmpty200Response, false);
QNetworkRequest request(QUrl("http://localhost:" + QString::number(server.serverPort())));
request.setRawHeader("Content-Type", "application/octet-stream");
QByteArray postData("Just some nonsense");
QNetworkReplyPtr reply(manager.post(request, postData));
QSignalSpy spy(reply.data(), SIGNAL(error(QNetworkReply::NetworkError)));
QCOMPARE(waitForFinish(reply), int(Success));
QCOMPARE(spy.count(), 0);
QVERIFY(reply->error() == QNetworkReply::NoError);
request.setTransferTimeout(1000);
server.stopTransfer = true;
QNetworkReplyPtr reply2(manager.post(request, postData));
QSignalSpy spy2(reply2.data(), SIGNAL(error(QNetworkReply::NetworkError)));
QCOMPARE(waitForFinish(reply2), int(Failure));
QCOMPARE(spy2.count(), 1);
QVERIFY(reply2->error() == QNetworkReply::OperationCanceledError);
}
// NOTE: This test must be last testcase in tst_qnetworkreply!
void tst_QNetworkReply::parentingRepliesToTheApp()
{