QGIS API Documentation 3.41.0-Master (02257426e5a)
Loading...
Searching...
No Matches
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 "moc_qgsblockingnetworkrequest.cpp"
18#include "qgslogger.h"
19#include "qgsapplication.h"
21#include "qgsauthmanager.h"
22#include "qgsmessagelog.h"
23#include "qgsfeedback.h"
24#include "qgsvariantutils.h"
25#include <QUrl>
26#include <QNetworkRequest>
27#include <QNetworkReply>
28#include <QMutex>
29#include <QWaitCondition>
30#include <QNetworkCacheMetaData>
31#include <QAuthenticator>
32#include <QBuffer>
33
35{
36 connect( QgsNetworkAccessManager::instance(), qOverload< QNetworkReply * >( &QgsNetworkAccessManager::requestTimedOut ), this, &QgsBlockingNetworkRequest::requestTimedOut );
37}
38
43
44void QgsBlockingNetworkRequest::requestTimedOut( QNetworkReply *reply )
45{
46 if ( reply == mReply )
47 mTimedout = true;
48}
49
51{
52 return mAuthCfg;
53}
54
55void QgsBlockingNetworkRequest::setAuthCfg( const QString &authCfg )
56{
57 mAuthCfg = authCfg;
58}
59
60QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::get( QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback, RequestFlags requestFlags )
61{
62 return doRequest( Get, request, forceRefresh, feedback, requestFlags );
63}
64
65QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::post( QNetworkRequest &request, const QByteArray &data, bool forceRefresh, QgsFeedback *feedback )
66{
67 QByteArray ldata( data );
68 QBuffer buffer( &ldata );
69 buffer.open( QIODevice::ReadOnly );
70 return post( request, &buffer, forceRefresh, feedback );
71}
72
73QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::post( QNetworkRequest &request, QIODevice *data, bool forceRefresh, QgsFeedback *feedback )
74{
75 mPayloadData = data;
76 return doRequest( Post, request, forceRefresh, feedback );
77}
78
79QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::head( QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback )
80{
81 return doRequest( Head, request, forceRefresh, feedback );
82}
83
84QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::put( QNetworkRequest &request, const QByteArray &data, QgsFeedback *feedback )
85{
86 QByteArray ldata( data );
87 QBuffer buffer( &ldata );
88 buffer.open( QIODevice::ReadOnly );
89 return put( request, &buffer, feedback );
90}
91
92QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::put( QNetworkRequest &request, QIODevice *data, QgsFeedback *feedback )
93{
94 mPayloadData = data;
95 return doRequest( Put, request, true, feedback );
96}
97
99{
100 return doRequest( Delete, request, true, feedback );
101}
102
103void QgsBlockingNetworkRequest::sendRequestToNetworkAccessManager( const QNetworkRequest &request )
104{
105 switch ( mMethod )
106 {
107 case Get:
108 mReply = QgsNetworkAccessManager::instance()->get( request );
109 break;
110
111 case Post:
112 mReply = QgsNetworkAccessManager::instance()->post( request, mPayloadData );
113 break;
114
115 case Head:
116 mReply = QgsNetworkAccessManager::instance()->head( request );
117 break;
118
119 case Put:
120 mReply = QgsNetworkAccessManager::instance()->put( request, mPayloadData );
121 break;
122
123 case Delete:
124 mReply = QgsNetworkAccessManager::instance()->deleteResource( request );
125 break;
126 };
127}
128
129QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::doRequest( QgsBlockingNetworkRequest::Method method, QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback, RequestFlags requestFlags )
130{
131 mMethod = method;
132 mFeedback = feedback;
133
134 abort(); // cancel previous
135 mIsAborted = false;
136 mTimedout = false;
137 mGotNonEmptyResponse = false;
138 mRequestFlags = requestFlags;
139
140 mErrorMessage.clear();
141 mErrorCode = NoError;
142 mForceRefresh = forceRefresh;
143 mReplyContent.clear();
144
145 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
146 {
147 mErrorCode = NetworkError;
148 mErrorMessage = errorMessageFailedAuth();
149 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
150 return NetworkError;
151 }
152
153 QgsDebugMsgLevel( QStringLiteral( "Calling: %1" ).arg( request.url().toString() ), 2 );
154
155 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
156 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, forceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
157 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
158
159 QWaitCondition authRequestBufferNotEmpty;
160 QMutex waitConditionMutex;
161
162 bool threadFinished = false;
163 bool success = false;
164
165 const bool requestMadeFromMainThread = QThread::currentThread() == QApplication::instance()->thread();
166
167 if ( mFeedback )
168 connect( mFeedback, &QgsFeedback::canceled, this, &QgsBlockingNetworkRequest::abort );
169
170 const std::function<void()> downloaderFunction = [ this, request, &waitConditionMutex, &authRequestBufferNotEmpty, &threadFinished, &success, requestMadeFromMainThread ]()
171 {
172 // this function will always be run in worker threads -- either the blocking call is being made in a worker thread,
173 // or the blocking call has been made from the main thread and we've fired up a new thread for this function
174 Q_ASSERT( QThread::currentThread() != QgsApplication::instance()->thread() );
175
176 QgsNetworkAccessManager::instance( Qt::DirectConnection );
177
178 success = true;
179
180 sendRequestToNetworkAccessManager( request );
181
182 if ( mFeedback )
183 connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
184
185 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
186 {
187 mErrorCode = NetworkError;
188 mErrorMessage = errorMessageFailedAuth();
189 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
190 if ( requestMadeFromMainThread )
191 authRequestBufferNotEmpty.wakeAll();
192 success = false;
193 }
194 else
195 {
196 // We are able to use direct connection here, because we
197 // * either run on the thread mReply lives in, so DirectConnection is standard and safe anyway
198 // * or the owner thread of mReply is currently not doing anything because it's blocked in future.waitForFinished() (if it is the main thread)
199 connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
200 connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
201 connect( mReply, &QNetworkReply::uploadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
202
203 if ( request.hasRawHeader( "Range" ) )
204 connect( mReply, &QNetworkReply::metaDataChanged, this, &QgsBlockingNetworkRequest::abortIfNotPartialContentReturned, Qt::DirectConnection );
205
206 auto resumeMainThread = [&waitConditionMutex, &authRequestBufferNotEmpty ]()
207 {
208 // when this method is called we have "produced" a single authentication request -- so the buffer is now full
209 // and it's time for the "consumer" (main thread) to do its part
210 waitConditionMutex.lock();
211 authRequestBufferNotEmpty.wakeAll();
212 waitConditionMutex.unlock();
213
214 // note that we don't need to handle waking this thread back up - that's done automatically by QgsNetworkAccessManager
215 };
216
217 QMetaObject::Connection authRequestConnection;
218 QMetaObject::Connection proxyAuthenticationConnection;
219#ifndef QT_NO_SSL
220 QMetaObject::Connection sslErrorsConnection;
221#endif
222
223 if ( requestMadeFromMainThread )
224 {
225 authRequestConnection = connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::authRequestOccurred, this, resumeMainThread, Qt::DirectConnection );
226 proxyAuthenticationConnection = connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::proxyAuthenticationRequired, this, resumeMainThread, Qt::DirectConnection );
227
228#ifndef QT_NO_SSL
229 sslErrorsConnection = connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::sslErrorsOccurred, this, resumeMainThread, Qt::DirectConnection );
230#endif
231 }
232 QEventLoop loop;
233 // connecting to aboutToQuit avoids an on-going request to remain stalled
234 // when QThreadPool::globalInstance()->waitForDone()
235 // is called at process termination
236 connect( qApp, &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit, Qt::DirectConnection );
237 connect( this, &QgsBlockingNetworkRequest::finished, &loop, &QEventLoop::quit, Qt::DirectConnection );
238 loop.exec();
239
240 if ( requestMadeFromMainThread )
241 {
242 // event loop exited - need to disconnect as to not leave functor hanging to receive signals in future
243 disconnect( authRequestConnection );
244 disconnect( proxyAuthenticationConnection );
245#ifndef QT_NO_SSL
246 disconnect( sslErrorsConnection );
247#endif
248 }
249 }
250
251 if ( requestMadeFromMainThread )
252 {
253 waitConditionMutex.lock();
254 threadFinished = true;
255 authRequestBufferNotEmpty.wakeAll();
256 waitConditionMutex.unlock();
257 }
258 };
259
260 if ( requestMadeFromMainThread )
261 {
262 std::unique_ptr<DownloaderThread> downloaderThread = std::make_unique<DownloaderThread>( downloaderFunction );
263 downloaderThread->start();
264
265 while ( true )
266 {
267 waitConditionMutex.lock();
268 if ( threadFinished )
269 {
270 waitConditionMutex.unlock();
271 break;
272 }
273 authRequestBufferNotEmpty.wait( &waitConditionMutex );
274
275 // If the downloader thread wakes us (the main thread) up and is not yet finished
276 // then it has "produced" an authentication request which we need to now "consume".
277 // The processEvents() call gives the auth manager the chance to show a dialog and
278 // once done with that, we can wake the downloaderThread again and continue the download.
279 if ( !threadFinished )
280 {
281 waitConditionMutex.unlock();
282
283 QgsApplication::processEvents();
284 // we don't need to wake up the worker thread - it will automatically be woken when
285 // the auth request has been dealt with by QgsNetworkAccessManager
286 }
287 else
288 {
289 waitConditionMutex.unlock();
290 }
291 }
292 // wait for thread to gracefully exit
293 downloaderThread->wait();
294 }
295 else
296 {
297 downloaderFunction();
298 }
299 return mErrorCode;
300}
301
303{
304 mIsAborted = true;
305 if ( mReply )
306 {
307 mReply->deleteLater();
308 mReply = nullptr;
309 }
310}
311
312void QgsBlockingNetworkRequest::replyProgress( qint64 bytesReceived, qint64 bytesTotal )
313{
314 QgsDebugMsgLevel( QStringLiteral( "%1 of %2 bytes downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QStringLiteral( "unknown number of" ) : QString::number( bytesTotal ) ), 2 );
315
316 if ( bytesReceived != 0 )
317 mGotNonEmptyResponse = true;
318
319 if ( !mIsAborted && mReply && ( !mFeedback || !mFeedback->isCanceled() ) )
320 {
321 if ( mReply->error() == QNetworkReply::NoError )
322 {
323 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
324 if ( !QgsVariantUtils::isNull( redirect ) )
325 {
326 // We don't want to emit downloadProgress() for a redirect
327 return;
328 }
329 }
330 }
331
332 if ( mMethod == Put || mMethod == Post )
333 emit uploadProgress( bytesReceived, bytesTotal );
334 else
335 emit downloadProgress( bytesReceived, bytesTotal );
336}
337
338void QgsBlockingNetworkRequest::replyFinished()
339{
340 if ( !mIsAborted && mReply )
341 {
342
343 if ( mReply->error() == QNetworkReply::NoError && ( !mFeedback || !mFeedback->isCanceled() ) )
344 {
345 QgsDebugMsgLevel( QStringLiteral( "reply OK" ), 2 );
346 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
347 if ( !QgsVariantUtils::isNull( redirect ) )
348 {
349 QgsDebugMsgLevel( QStringLiteral( "Request redirected." ), 2 );
350
351 const QUrl &toUrl = redirect.toUrl();
352 mReply->request();
353 if ( toUrl == mReply->url() )
354 {
355 mErrorMessage = tr( "Redirect loop detected: %1" ).arg( toUrl.toString() );
356 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
357 mReplyContent.clear();
358 }
359 else
360 {
361 QNetworkRequest request( toUrl );
362
363 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
364 {
365 mReplyContent.clear();
366 mErrorMessage = errorMessageFailedAuth();
367 mErrorCode = NetworkError;
368 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
369 emit finished();
371 emit downloadFinished();
373 return;
374 }
375
376 request.setAttribute( QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy );
377 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, mForceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
378 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
379
380 // if that was a range request, use the same range for the redirected request
381 if ( mReply->request().hasRawHeader( "Range" ) )
382 request.setRawHeader( "Range", mReply->request().rawHeader( "Range" ) );
383
384 mReply->deleteLater();
385 mReply = nullptr;
386
387 QgsDebugMsgLevel( QStringLiteral( "redirected: %1 forceRefresh=%2" ).arg( redirect.toString() ).arg( mForceRefresh ), 2 );
388
389 sendRequestToNetworkAccessManager( request );
390
391 if ( mFeedback )
392 connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
393
394 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
395 {
396 mReplyContent.clear();
397 mErrorMessage = errorMessageFailedAuth();
398 mErrorCode = NetworkError;
399 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
400 emit finished();
402 emit downloadFinished();
404 return;
405 }
406
407 connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
408 connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
409 connect( mReply, &QNetworkReply::uploadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
410
411 if ( request.hasRawHeader( "Range" ) )
412 connect( mReply, &QNetworkReply::metaDataChanged, this, &QgsBlockingNetworkRequest::abortIfNotPartialContentReturned, Qt::DirectConnection );
413
414 return;
415 }
416 }
417 else
418 {
420
421 if ( nam->cache() )
422 {
423 QNetworkCacheMetaData cmd = nam->cache()->metaData( mReply->request().url() );
424
425 QNetworkCacheMetaData::RawHeaderList hl;
426 const auto constRawHeaders = cmd.rawHeaders();
427 for ( const QNetworkCacheMetaData::RawHeader &h : constRawHeaders )
428 {
429 if ( h.first != "Cache-Control" )
430 hl.append( h );
431 }
432 cmd.setRawHeaders( hl );
433
434 QgsDebugMsgLevel( QStringLiteral( "expirationDate:%1" ).arg( cmd.expirationDate().toString() ), 2 );
435 if ( cmd.expirationDate().isNull() )
436 {
437 cmd.setExpirationDate( QDateTime::currentDateTime().addSecs( mExpirationSec ) );
438 }
439
440 nam->cache()->updateMetaData( cmd );
441 }
442 else
443 {
444 QgsDebugMsgLevel( QStringLiteral( "No cache!" ), 2 );
445 }
446
447#ifdef QGISDEBUG
448 const bool fromCache = mReply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ).toBool();
449 QgsDebugMsgLevel( QStringLiteral( "Reply was cached: %1" ).arg( fromCache ), 2 );
450#endif
451
452 mReplyContent = QgsNetworkReplyContent( mReply );
453 const QByteArray content = mReply->readAll();
454 if ( !( mRequestFlags & RequestFlag::EmptyResponseIsValid ) && content.isEmpty() && !mGotNonEmptyResponse && mMethod == Get )
455 {
456 mErrorMessage = tr( "empty response: %1" ).arg( mReply->errorString() );
457 mErrorCode = ServerExceptionError;
458 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
459 }
460 mReplyContent.setContent( content );
461 }
462 }
463 else
464 {
465 if ( mReply->error() != QNetworkReply::OperationCanceledError )
466 {
467 mErrorMessage = mReply->errorString();
468 mErrorCode = ServerExceptionError;
469 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
470 }
471 mReplyContent = QgsNetworkReplyContent( mReply );
472 mReplyContent.setContent( mReply->readAll() );
473 }
474 }
475 if ( mTimedout )
476 mErrorCode = TimeoutError;
477
478 if ( mReply )
479 {
480 mReply->deleteLater();
481 mReply = nullptr;
482 }
483
484 emit finished();
486 emit downloadFinished();
488}
489
490QString QgsBlockingNetworkRequest::errorMessageFailedAuth()
491{
492 return tr( "network request update failed for authentication config" );
493}
494
495void QgsBlockingNetworkRequest::abortIfNotPartialContentReturned()
496{
497 if ( mReply && mReply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt() == 200 )
498 {
499 // We're expecting a 206 - Partial Content but the server returned 200
500 // It seems it does not support range requests and is returning the whole file!
501 mReply->abort();
502 mErrorMessage = tr( "The server does not support range requests" );
503 mErrorCode = ServerExceptionError;
504 }
505}
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,...
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.
void uploadProgress(qint64 bytesReceived, qint64 bytesTotal)
Emitted when when data are sent during a request.
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 downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
Emitted when when data arrives during a request.
ErrorCode get(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr, RequestFlags requestFlags=QgsBlockingNetworkRequest::RequestFlags())
Performs a "get" operation on the specified request.
@ EmptyResponseIsValid
Do not generate an error if getting an empty response (e.g. HTTP 204)
@ 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, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE())
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:6668
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:6667
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:41