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
parent
5d4b5dab7f
commit
c33d8bfc99
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue