summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVille Voutilainen <ville.voutilainen@qt.io>2019-10-17 17:38:53 +0300
committerVille Voutilainen <ville.voutilainen@qt.io>2019-10-28 21:15:43 +0300
commitc33d8bfc996cb52d3b9d5b821e51860347c74fa2 (patch)
tree6f069889758764f77c3b9a928d1bc78ebaf98113
parent5d4b5dab7fec0f2a511145209daea9a85e741cc7 (diff)
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>
-rw-r--r--src/network/access/qnetworkreplyhttpimpl.cpp36
-rw-r--r--src/network/access/qnetworkreplyhttpimpl_p.h7
-rw-r--r--src/network/access/qnetworkrequest.cpp50
-rw-r--r--src/network/access/qnetworkrequest.h6
-rw-r--r--tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp60
5 files changed, 154 insertions, 5 deletions
diff --git a/src/network/access/qnetworkreplyhttpimpl.cpp b/src/network/access/qnetworkreplyhttpimpl.cpp
index 44c1d3e422..a13c2b144c 100644
--- a/src/network/access/qnetworkreplyhttpimpl.cpp
+++ b/src/network/access/qnetworkreplyhttpimpl.cpp
@@ -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;
diff --git a/src/network/access/qnetworkreplyhttpimpl_p.h b/src/network/access/qnetworkreplyhttpimpl_p.h
index ef69ce0653..dec0c4c589 100644
--- a/src/network/access/qnetworkreplyhttpimpl_p.h
+++ b/src/network/access/qnetworkreplyhttpimpl_p.h
@@ -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;
diff --git a/src/network/access/qnetworkrequest.cpp b/src/network/access/qnetworkrequest.cpp
index 7899bce32b..e03d844af9 100644
--- a/src/network/access/qnetworkrequest.cpp
+++ b/src/network/access/qnetworkrequest.cpp
@@ -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)
diff --git a/src/network/access/qnetworkrequest.h b/src/network/access/qnetworkrequest.h
index 72a6555d91..5d9969bd9b 100644
--- a/src/network/access/qnetworkrequest.h
+++ b/src/network/access/qnetworkrequest.h
@@ -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;
diff --git a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp
index 418e1caf68..3bc6717d58 100644
--- a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp
+++ b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp
@@ -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()
{