QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
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 auto resumeMainThread = [&waitConditionMutex, &authRequestBufferNotEmpty ]()
201 {
202 // when this method is called we have "produced" a single authentication request -- so the buffer is now full
203 // and it's time for the "consumer" (main thread) to do its part
204 waitConditionMutex.lock();
205 authRequestBufferNotEmpty.wakeAll();
206 waitConditionMutex.unlock();
207
208 // note that we don't need to handle waking this thread back up - that's done automatically by QgsNetworkAccessManager
209 };
210
211 if ( requestMadeFromMainThread )
212 {
213 connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::authRequestOccurred, this, resumeMainThread, Qt::DirectConnection );
214 connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::proxyAuthenticationRequired, this, resumeMainThread, Qt::DirectConnection );
215
216#ifndef QT_NO_SSL
217 connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::sslErrorsOccurred, this, resumeMainThread, Qt::DirectConnection );
218#endif
219 }
220 QEventLoop loop;
221 // connecting to aboutToQuit avoids an on-going request to remain stalled
222 // when QThreadPool::globalInstance()->waitForDone()
223 // is called at process termination
224 connect( qApp, &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit, Qt::DirectConnection );
225 connect( this, &QgsBlockingNetworkRequest::finished, &loop, &QEventLoop::quit, Qt::DirectConnection );
226 loop.exec();
227 }
228
229 if ( requestMadeFromMainThread )
230 {
231 waitConditionMutex.lock();
232 threadFinished = true;
233 authRequestBufferNotEmpty.wakeAll();
234 waitConditionMutex.unlock();
235 }
236 };
237
238 if ( requestMadeFromMainThread )
239 {
240 std::unique_ptr<DownloaderThread> downloaderThread = std::make_unique<DownloaderThread>( downloaderFunction );
241 downloaderThread->start();
242
243 while ( true )
244 {
245 waitConditionMutex.lock();
246 if ( threadFinished )
247 {
248 waitConditionMutex.unlock();
249 break;
250 }
251 authRequestBufferNotEmpty.wait( &waitConditionMutex );
252
253 // If the downloader thread wakes us (the main thread) up and is not yet finished
254 // then it has "produced" an authentication request which we need to now "consume".
255 // The processEvents() call gives the auth manager the chance to show a dialog and
256 // once done with that, we can wake the downloaderThread again and continue the download.
257 if ( !threadFinished )
258 {
259 waitConditionMutex.unlock();
260
261 QgsApplication::processEvents();
262 // we don't need to wake up the worker thread - it will automatically be woken when
263 // the auth request has been dealt with by QgsNetworkAccessManager
264 }
265 else
266 {
267 waitConditionMutex.unlock();
268 }
269 }
270 // wait for thread to gracefully exit
271 downloaderThread->wait();
272 }
273 else
274 {
275 downloaderFunction();
276 }
277 return mErrorCode;
278}
279
281{
282 mIsAborted = true;
283 if ( mReply )
284 {
285 mReply->deleteLater();
286 mReply = nullptr;
287 }
288}
289
290void QgsBlockingNetworkRequest::replyProgress( qint64 bytesReceived, qint64 bytesTotal )
291{
292 QgsDebugMsgLevel( QStringLiteral( "%1 of %2 bytes downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QStringLiteral( "unknown number of" ) : QString::number( bytesTotal ) ), 2 );
293
294 if ( bytesReceived != 0 )
295 mGotNonEmptyResponse = true;
296
297 if ( !mIsAborted && mReply && ( !mFeedback || !mFeedback->isCanceled() ) )
298 {
299 if ( mReply->error() == QNetworkReply::NoError )
300 {
301 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
302 if ( !QgsVariantUtils::isNull( redirect ) )
303 {
304 // We don't want to emit downloadProgress() for a redirect
305 return;
306 }
307 }
308 }
309
310 if ( mMethod == Put || mMethod == Post )
311 emit uploadProgress( bytesReceived, bytesTotal );
312 else
313 emit downloadProgress( bytesReceived, bytesTotal );
314}
315
316void QgsBlockingNetworkRequest::replyFinished()
317{
318 if ( !mIsAborted && mReply )
319 {
320
321 if ( mReply->error() == QNetworkReply::NoError && ( !mFeedback || !mFeedback->isCanceled() ) )
322 {
323 QgsDebugMsgLevel( QStringLiteral( "reply OK" ), 2 );
324 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
325 if ( !QgsVariantUtils::isNull( redirect ) )
326 {
327 QgsDebugMsgLevel( QStringLiteral( "Request redirected." ), 2 );
328
329 const QUrl &toUrl = redirect.toUrl();
330 mReply->request();
331 if ( toUrl == mReply->url() )
332 {
333 mErrorMessage = tr( "Redirect loop detected: %1" ).arg( toUrl.toString() );
334 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
335 mReplyContent.clear();
336 }
337 else
338 {
339 QNetworkRequest request( toUrl );
340
341 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
342 {
343 mReplyContent.clear();
344 mErrorMessage = errorMessageFailedAuth();
345 mErrorCode = NetworkError;
346 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
347 emit finished();
349 emit downloadFinished();
351 return;
352 }
353
354 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, mForceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
355 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
356
357 mReply->deleteLater();
358 mReply = nullptr;
359
360 QgsDebugMsgLevel( QStringLiteral( "redirected: %1 forceRefresh=%2" ).arg( redirect.toString() ).arg( mForceRefresh ), 2 );
361
362 sendRequestToNetworkAccessManager( request );
363
364 if ( mFeedback )
365 connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
366
367 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
368 {
369 mReplyContent.clear();
370 mErrorMessage = errorMessageFailedAuth();
371 mErrorCode = NetworkError;
372 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
373 emit finished();
375 emit downloadFinished();
377 return;
378 }
379
380 connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
381 connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
382 connect( mReply, &QNetworkReply::uploadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
383 return;
384 }
385 }
386 else
387 {
389
390 if ( nam->cache() )
391 {
392 QNetworkCacheMetaData cmd = nam->cache()->metaData( mReply->request().url() );
393
394 QNetworkCacheMetaData::RawHeaderList hl;
395 const auto constRawHeaders = cmd.rawHeaders();
396 for ( const QNetworkCacheMetaData::RawHeader &h : constRawHeaders )
397 {
398 if ( h.first != "Cache-Control" )
399 hl.append( h );
400 }
401 cmd.setRawHeaders( hl );
402
403 QgsDebugMsgLevel( QStringLiteral( "expirationDate:%1" ).arg( cmd.expirationDate().toString() ), 2 );
404 if ( cmd.expirationDate().isNull() )
405 {
406 cmd.setExpirationDate( QDateTime::currentDateTime().addSecs( mExpirationSec ) );
407 }
408
409 nam->cache()->updateMetaData( cmd );
410 }
411 else
412 {
413 QgsDebugMsgLevel( QStringLiteral( "No cache!" ), 2 );
414 }
415
416#ifdef QGISDEBUG
417 const bool fromCache = mReply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ).toBool();
418 QgsDebugMsgLevel( QStringLiteral( "Reply was cached: %1" ).arg( fromCache ), 2 );
419#endif
420
421 mReplyContent = QgsNetworkReplyContent( mReply );
422 const QByteArray content = mReply->readAll();
423 if ( content.isEmpty() && !mGotNonEmptyResponse && mMethod == Get )
424 {
425 mErrorMessage = tr( "empty response: %1" ).arg( mReply->errorString() );
426 mErrorCode = ServerExceptionError;
427 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
428 }
429 mReplyContent.setContent( content );
430 }
431 }
432 else
433 {
434 if ( mReply->error() != QNetworkReply::OperationCanceledError )
435 {
436 mErrorMessage = mReply->errorString();
437 mErrorCode = ServerExceptionError;
438 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
439 }
440 mReplyContent = QgsNetworkReplyContent( mReply );
441 mReplyContent.setContent( mReply->readAll() );
442 }
443 }
444 if ( mTimedout )
445 mErrorCode = TimeoutError;
446
447 if ( mReply )
448 {
449 mReply->deleteLater();
450 mReply = nullptr;
451 }
452
453 emit finished();
455 emit downloadFinished();
457}
458
459QString QgsBlockingNetworkRequest::errorMessageFailedAuth()
460{
461 return tr( "network request update failed for authentication config" );
462}
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:45
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)
Returns true if the specified variant should be considered a NULL value.
#define Q_NOWARN_DEPRECATED_POP
Definition: qgis.h:3061
#define Q_NOWARN_DEPRECATED_PUSH
Definition: qgis.h:3060
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39