QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgsblockingnetworkrequest.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsblockingnetworkrequest.cpp
3 -----------------------------
4 begin : November 2018
5 copyright : (C) 2018 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17#include "qgslogger.h"
18#include "qgsapplication.h"
20#include "qgsauthmanager.h"
21#include "qgsmessagelog.h"
22#include "qgsfeedback.h"
23#include "qgsvariantutils.h"
24#include <QUrl>
25#include <QNetworkRequest>
26#include <QNetworkReply>
27#include <QMutex>
28#include <QWaitCondition>
29#include <QNetworkCacheMetaData>
30#include <QAuthenticator>
31#include <QBuffer>
32
34{
35 connect( QgsNetworkAccessManager::instance(), qOverload< QNetworkReply * >( &QgsNetworkAccessManager::requestTimedOut ), this, &QgsBlockingNetworkRequest::requestTimedOut );
36}
37
39{
40 abort();
41}
42
43void QgsBlockingNetworkRequest::requestTimedOut( QNetworkReply *reply )
44{
45 if ( reply == mReply )
46 mTimedout = true;
47}
48
50{
51 return mAuthCfg;
52}
53
54void QgsBlockingNetworkRequest::setAuthCfg( const QString &authCfg )
55{
56 mAuthCfg = authCfg;
57}
58
59QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::get( QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback )
60{
61 return doRequest( Get, request, forceRefresh, feedback );
62}
63
64QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::post( QNetworkRequest &request, const QByteArray &data, bool forceRefresh, QgsFeedback *feedback )
65{
66 QByteArray ldata( data );
67 QBuffer buffer( &ldata );
68 buffer.open( QIODevice::ReadOnly );
69 return post( request, &buffer, forceRefresh, feedback );
70}
71
72QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::post( QNetworkRequest &request, QIODevice *data, bool forceRefresh, QgsFeedback *feedback )
73{
74 mPayloadData = data;
75 return doRequest( Post, request, forceRefresh, feedback );
76}
77
78QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::head( QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback )
79{
80 return doRequest( Head, request, forceRefresh, feedback );
81}
82
83QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::put( QNetworkRequest &request, const QByteArray &data, QgsFeedback *feedback )
84{
85 QByteArray ldata( data );
86 QBuffer buffer( &ldata );
87 buffer.open( QIODevice::ReadOnly );
88 return put( request, &buffer, feedback );
89}
90
91QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::put( QNetworkRequest &request, QIODevice *data, QgsFeedback *feedback )
92{
93 mPayloadData = data;
94 return doRequest( Put, request, true, feedback );
95}
96
98{
99 return doRequest( Delete, request, true, feedback );
100}
101
102void QgsBlockingNetworkRequest::sendRequestToNetworkAccessManager( const QNetworkRequest &request )
103{
104 switch ( mMethod )
105 {
106 case Get:
107 mReply = QgsNetworkAccessManager::instance()->get( request );
108 break;
109
110 case Post:
111 mReply = QgsNetworkAccessManager::instance()->post( request, mPayloadData );
112 break;
113
114 case Head:
115 mReply = QgsNetworkAccessManager::instance()->head( request );
116 break;
117
118 case Put:
119 mReply = QgsNetworkAccessManager::instance()->put( request, mPayloadData );
120 break;
121
122 case Delete:
123 mReply = QgsNetworkAccessManager::instance()->deleteResource( request );
124 break;
125 };
126}
127
128QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::doRequest( QgsBlockingNetworkRequest::Method method, QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback )
129{
130 mMethod = method;
131 mFeedback = feedback;
132
133 abort(); // cancel previous
134 mIsAborted = false;
135 mTimedout = false;
136 mGotNonEmptyResponse = false;
137
138 mErrorMessage.clear();
139 mErrorCode = NoError;
140 mForceRefresh = forceRefresh;
141 mReplyContent.clear();
142
143 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
144 {
145 mErrorCode = NetworkError;
146 mErrorMessage = errorMessageFailedAuth();
147 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
148 return NetworkError;
149 }
150
151 QgsDebugMsgLevel( QStringLiteral( "Calling: %1" ).arg( request.url().toString() ), 2 );
152
153 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, forceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
154 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
155
156 QWaitCondition authRequestBufferNotEmpty;
157 QMutex waitConditionMutex;
158
159 bool threadFinished = false;
160 bool success = false;
161
162 const bool requestMadeFromMainThread = QThread::currentThread() == QApplication::instance()->thread();
163
164 if ( mFeedback )
165 connect( mFeedback, &QgsFeedback::canceled, this, &QgsBlockingNetworkRequest::abort );
166
167 const std::function<void()> downloaderFunction = [ this, request, &waitConditionMutex, &authRequestBufferNotEmpty, &threadFinished, &success, requestMadeFromMainThread ]()
168 {
169 // this function will always be run in worker threads -- either the blocking call is being made in a worker thread,
170 // or the blocking call has been made from the main thread and we've fired up a new thread for this function
171 Q_ASSERT( QThread::currentThread() != QgsApplication::instance()->thread() );
172
173 QgsNetworkAccessManager::instance( Qt::DirectConnection );
174
175 success = true;
176
177 sendRequestToNetworkAccessManager( request );
178
179 if ( mFeedback )
180 connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
181
182 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
183 {
184 mErrorCode = NetworkError;
185 mErrorMessage = errorMessageFailedAuth();
186 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
187 if ( requestMadeFromMainThread )
188 authRequestBufferNotEmpty.wakeAll();
189 success = false;
190 }
191 else
192 {
193 // We are able to use direct connection here, because we
194 // * either run on the thread mReply lives in, so DirectConnection is standard and safe anyway
195 // * or the owner thread of mReply is currently not doing anything because it's blocked in future.waitForFinished() (if it is the main thread)
196 connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
197 connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
198 connect( mReply, &QNetworkReply::uploadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
199
200 if ( request.hasRawHeader( "Range" ) )
201 connect( mReply, &QNetworkReply::metaDataChanged, this, &QgsBlockingNetworkRequest::abortIfNotPartialContentReturned, Qt::DirectConnection );
202
203 auto resumeMainThread = [&waitConditionMutex, &authRequestBufferNotEmpty ]()
204 {
205 // when this method is called we have "produced" a single authentication request -- so the buffer is now full
206 // and it's time for the "consumer" (main thread) to do its part
207 waitConditionMutex.lock();
208 authRequestBufferNotEmpty.wakeAll();
209 waitConditionMutex.unlock();
210
211 // note that we don't need to handle waking this thread back up - that's done automatically by QgsNetworkAccessManager
212 };
213
214 if ( requestMadeFromMainThread )
215 {
216 connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::authRequestOccurred, this, resumeMainThread, Qt::DirectConnection );
217 connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::proxyAuthenticationRequired, this, resumeMainThread, Qt::DirectConnection );
218
219#ifndef QT_NO_SSL
220 connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::sslErrorsOccurred, this, resumeMainThread, Qt::DirectConnection );
221#endif
222 }
223 QEventLoop loop;
224 // connecting to aboutToQuit avoids an on-going request to remain stalled
225 // when QThreadPool::globalInstance()->waitForDone()
226 // is called at process termination
227 connect( qApp, &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit, Qt::DirectConnection );
228 connect( this, &QgsBlockingNetworkRequest::finished, &loop, &QEventLoop::quit, Qt::DirectConnection );
229 loop.exec();
230 }
231
232 if ( requestMadeFromMainThread )
233 {
234 waitConditionMutex.lock();
235 threadFinished = true;
236 authRequestBufferNotEmpty.wakeAll();
237 waitConditionMutex.unlock();
238 }
239 };
240
241 if ( requestMadeFromMainThread )
242 {
243 std::unique_ptr<DownloaderThread> downloaderThread = std::make_unique<DownloaderThread>( downloaderFunction );
244 downloaderThread->start();
245
246 while ( true )
247 {
248 waitConditionMutex.lock();
249 if ( threadFinished )
250 {
251 waitConditionMutex.unlock();
252 break;
253 }
254 authRequestBufferNotEmpty.wait( &waitConditionMutex );
255
256 // If the downloader thread wakes us (the main thread) up and is not yet finished
257 // then it has "produced" an authentication request which we need to now "consume".
258 // The processEvents() call gives the auth manager the chance to show a dialog and
259 // once done with that, we can wake the downloaderThread again and continue the download.
260 if ( !threadFinished )
261 {
262 waitConditionMutex.unlock();
263
264 QgsApplication::processEvents();
265 // we don't need to wake up the worker thread - it will automatically be woken when
266 // the auth request has been dealt with by QgsNetworkAccessManager
267 }
268 else
269 {
270 waitConditionMutex.unlock();
271 }
272 }
273 // wait for thread to gracefully exit
274 downloaderThread->wait();
275 }
276 else
277 {
278 downloaderFunction();
279 }
280 return mErrorCode;
281}
282
284{
285 mIsAborted = true;
286 if ( mReply )
287 {
288 mReply->deleteLater();
289 mReply = nullptr;
290 }
291}
292
293void QgsBlockingNetworkRequest::replyProgress( qint64 bytesReceived, qint64 bytesTotal )
294{
295 QgsDebugMsgLevel( QStringLiteral( "%1 of %2 bytes downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QStringLiteral( "unknown number of" ) : QString::number( bytesTotal ) ), 2 );
296
297 if ( bytesReceived != 0 )
298 mGotNonEmptyResponse = true;
299
300 if ( !mIsAborted && mReply && ( !mFeedback || !mFeedback->isCanceled() ) )
301 {
302 if ( mReply->error() == QNetworkReply::NoError )
303 {
304 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
305 if ( !QgsVariantUtils::isNull( redirect ) )
306 {
307 // We don't want to emit downloadProgress() for a redirect
308 return;
309 }
310 }
311 }
312
313 if ( mMethod == Put || mMethod == Post )
314 emit uploadProgress( bytesReceived, bytesTotal );
315 else
316 emit downloadProgress( bytesReceived, bytesTotal );
317}
318
319void QgsBlockingNetworkRequest::replyFinished()
320{
321 if ( !mIsAborted && mReply )
322 {
323
324 if ( mReply->error() == QNetworkReply::NoError && ( !mFeedback || !mFeedback->isCanceled() ) )
325 {
326 QgsDebugMsgLevel( QStringLiteral( "reply OK" ), 2 );
327 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
328 if ( !QgsVariantUtils::isNull( redirect ) )
329 {
330 QgsDebugMsgLevel( QStringLiteral( "Request redirected." ), 2 );
331
332 const QUrl &toUrl = redirect.toUrl();
333 mReply->request();
334 if ( toUrl == mReply->url() )
335 {
336 mErrorMessage = tr( "Redirect loop detected: %1" ).arg( toUrl.toString() );
337 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
338 mReplyContent.clear();
339 }
340 else
341 {
342 QNetworkRequest request( toUrl );
343
344 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
345 {
346 mReplyContent.clear();
347 mErrorMessage = errorMessageFailedAuth();
348 mErrorCode = NetworkError;
349 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
350 emit finished();
352 emit downloadFinished();
354 return;
355 }
356
357 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, mForceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
358 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
359
360 // if that was a range request, use the same range for the redirected request
361 if ( mReply->request().hasRawHeader( "Range" ) )
362 request.setRawHeader( "Range", mReply->request().rawHeader( "Range" ) );
363
364 mReply->deleteLater();
365 mReply = nullptr;
366
367 QgsDebugMsgLevel( QStringLiteral( "redirected: %1 forceRefresh=%2" ).arg( redirect.toString() ).arg( mForceRefresh ), 2 );
368
369 sendRequestToNetworkAccessManager( request );
370
371 if ( mFeedback )
372 connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
373
374 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
375 {
376 mReplyContent.clear();
377 mErrorMessage = errorMessageFailedAuth();
378 mErrorCode = NetworkError;
379 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
380 emit finished();
382 emit downloadFinished();
384 return;
385 }
386
387 connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
388 connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
389 connect( mReply, &QNetworkReply::uploadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
390
391 if ( request.hasRawHeader( "Range" ) )
392 connect( mReply, &QNetworkReply::metaDataChanged, this, &QgsBlockingNetworkRequest::abortIfNotPartialContentReturned, Qt::DirectConnection );
393
394 return;
395 }
396 }
397 else
398 {
400
401 if ( nam->cache() )
402 {
403 QNetworkCacheMetaData cmd = nam->cache()->metaData( mReply->request().url() );
404
405 QNetworkCacheMetaData::RawHeaderList hl;
406 const auto constRawHeaders = cmd.rawHeaders();
407 for ( const QNetworkCacheMetaData::RawHeader &h : constRawHeaders )
408 {
409 if ( h.first != "Cache-Control" )
410 hl.append( h );
411 }
412 cmd.setRawHeaders( hl );
413
414 QgsDebugMsgLevel( QStringLiteral( "expirationDate:%1" ).arg( cmd.expirationDate().toString() ), 2 );
415 if ( cmd.expirationDate().isNull() )
416 {
417 cmd.setExpirationDate( QDateTime::currentDateTime().addSecs( mExpirationSec ) );
418 }
419
420 nam->cache()->updateMetaData( cmd );
421 }
422 else
423 {
424 QgsDebugMsgLevel( QStringLiteral( "No cache!" ), 2 );
425 }
426
427#ifdef QGISDEBUG
428 const bool fromCache = mReply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ).toBool();
429 QgsDebugMsgLevel( QStringLiteral( "Reply was cached: %1" ).arg( fromCache ), 2 );
430#endif
431
432 mReplyContent = QgsNetworkReplyContent( mReply );
433 const QByteArray content = mReply->readAll();
434 if ( content.isEmpty() && !mGotNonEmptyResponse && mMethod == Get )
435 {
436 mErrorMessage = tr( "empty response: %1" ).arg( mReply->errorString() );
437 mErrorCode = ServerExceptionError;
438 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
439 }
440 mReplyContent.setContent( content );
441 }
442 }
443 else
444 {
445 if ( mReply->error() != QNetworkReply::OperationCanceledError )
446 {
447 mErrorMessage = mReply->errorString();
448 mErrorCode = ServerExceptionError;
449 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
450 }
451 mReplyContent = QgsNetworkReplyContent( mReply );
452 mReplyContent.setContent( mReply->readAll() );
453 }
454 }
455 if ( mTimedout )
456 mErrorCode = TimeoutError;
457
458 if ( mReply )
459 {
460 mReply->deleteLater();
461 mReply = nullptr;
462 }
463
464 emit finished();
466 emit downloadFinished();
468}
469
470QString QgsBlockingNetworkRequest::errorMessageFailedAuth()
471{
472 return tr( "network request update failed for authentication config" );
473}
474
475void QgsBlockingNetworkRequest::abortIfNotPartialContentReturned()
476{
477 if ( mReply && mReply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt() == 200 )
478 {
479 // We're expecting a 206 - Partial Content but the server returned 200
480 // It seems it does not support range requests and is returning the whole file!
481 mReply->abort();
482 mErrorMessage = tr( "The server does not support range requests" );
483 mErrorCode = ServerExceptionError;
484 }
485}
static QgsApplication * instance()
Returns the singleton instance of the QgsApplication.
static QgsAuthManager * authManager()
Returns the application's authentication manager instance.
bool updateNetworkRequest(QNetworkRequest &request, const QString &authcfg, const QString &dataprovider=QString())
Provider call to update a QNetworkRequest with an authentication config.
bool updateNetworkReply(QNetworkReply *reply, const QString &authcfg, const QString &dataprovider=QString())
Provider call to update a QNetworkReply with an authentication config (used to skip known SSL errors,...
void downloadProgress(qint64, qint64)
Emitted when when data arrives during a request.
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "get" operation on the specified request.
QgsBlockingNetworkRequest()
Constructor for QgsBlockingNetworkRequest.
ErrorCode put(QNetworkRequest &request, QIODevice *data, QgsFeedback *feedback=nullptr)
Performs a "put" operation on the specified request, using the given data.
ErrorCode head(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "head" operation on the specified request.
void abort()
Aborts the network request immediately.
Q_DECL_DEPRECATED void downloadFinished()
Emitted once a request has finished downloading.
ErrorCode post(QNetworkRequest &request, QIODevice *data, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "post" operation on the specified request, using the given data.
ErrorCode deleteResource(QNetworkRequest &request, QgsFeedback *feedback=nullptr)
Performs a "delete" operation on the specified request.
void finished()
Emitted once a request has finished.
void setAuthCfg(const QString &authCfg)
Sets the authentication config id which should be used during the request.
QString authCfg() const
Returns the authentication config id which will be used during the request.
void uploadProgress(qint64, qint64)
Emitted when when data are sent during a request.
@ NetworkError
A network error occurred.
@ ServerExceptionError
An exception was raised by the server.
@ NoError
No error was encountered.
@ TimeoutError
Timeout was reached before a reply was received.
QgsNetworkReplyContent reply() const
Returns the content of the network reply, after a get(), post(), head() or put() request has been mad...
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition: qgsfeedback.h:44
void canceled()
Internal routines can connect to this signal if they use event loop.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
network access manager for QGIS
static QgsNetworkAccessManager * instance(Qt::ConnectionType connectionType=Qt::BlockingQueuedConnection)
Returns a pointer to the active QgsNetworkAccessManager for the current thread.
void requestTimedOut(QgsNetworkRequestParameters request)
Emitted when a network request has timed out.
Encapsulates a network reply within a container which is inexpensive to copy and safe to pass between...
void setContent(const QByteArray &content)
Sets the reply content.
void clear()
Clears the reply, resetting it back to a default, empty reply.
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
Returns true if the specified variant should be considered a NULL value.
#define Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:5776
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:5775
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39