QGIS API Documentation  3.18.1-Zürich (202f1bf7e5)
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 <QUrl>
24 #include <QNetworkRequest>
25 #include <QNetworkReply>
26 #include <QMutex>
27 #include <QWaitCondition>
28 #include <QNetworkCacheMetaData>
29 #include <QAuthenticator>
30 
32 {
33  connect( QgsNetworkAccessManager::instance(), qgis::overload< QNetworkReply * >::of( &QgsNetworkAccessManager::requestTimedOut ), this, &QgsBlockingNetworkRequest::requestTimedOut );
34 }
35 
37 {
38  abort();
39 }
40 
41 void QgsBlockingNetworkRequest::requestTimedOut( QNetworkReply *reply )
42 {
43  if ( reply == mReply )
44  mTimedout = true;
45 }
46 
48 {
49  return mAuthCfg;
50 }
51 
52 void QgsBlockingNetworkRequest::setAuthCfg( const QString &authCfg )
53 {
54  mAuthCfg = authCfg;
55 }
56 
57 QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::get( QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback )
58 {
59  return doRequest( Get, request, forceRefresh, feedback );
60 }
61 
62 QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::post( QNetworkRequest &request, const QByteArray &data, bool forceRefresh, QgsFeedback *feedback )
63 {
64  mPayloadData = data;
65  return doRequest( Post, request, forceRefresh, feedback );
66 }
67 
68 QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::head( QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback )
69 {
70  return doRequest( Head, request, forceRefresh, feedback );
71 }
72 
73 QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::put( QNetworkRequest &request, const QByteArray &data, QgsFeedback *feedback )
74 {
75  mPayloadData = data;
76  return doRequest( Put, request, true, feedback );
77 }
78 
80 {
81  return doRequest( Delete, request, true, feedback );
82 }
83 
84 void QgsBlockingNetworkRequest::sendRequestToNetworkAccessManager( const QNetworkRequest &request )
85 {
86  switch ( mMethod )
87  {
88  case Get:
89  mReply = QgsNetworkAccessManager::instance()->get( request );
90  break;
91 
92  case Post:
93  mReply = QgsNetworkAccessManager::instance()->post( request, mPayloadData );
94  break;
95 
96  case Head:
97  mReply = QgsNetworkAccessManager::instance()->head( request );
98  break;
99 
100  case Put:
101  mReply = QgsNetworkAccessManager::instance()->put( request, mPayloadData );
102  break;
103 
104  case Delete:
105  mReply = QgsNetworkAccessManager::instance()->deleteResource( request );
106  break;
107  };
108 }
109 
110 QgsBlockingNetworkRequest::ErrorCode QgsBlockingNetworkRequest::doRequest( QgsBlockingNetworkRequest::Method method, QNetworkRequest &request, bool forceRefresh, QgsFeedback *feedback )
111 {
112  mMethod = method;
113  mFeedback = feedback;
114 
115  abort(); // cancel previous
116  mIsAborted = false;
117  mTimedout = false;
118  mGotNonEmptyResponse = false;
119 
120  mErrorMessage.clear();
121  mErrorCode = NoError;
122  mForceRefresh = forceRefresh;
123  mReplyContent.clear();
124 
125  if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
126  {
127  mErrorCode = NetworkError;
128  mErrorMessage = errorMessageFailedAuth();
129  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
130  return NetworkError;
131  }
132 
133  QgsDebugMsgLevel( QStringLiteral( "Calling: %1" ).arg( request.url().toString() ), 2 );
134 
135  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, forceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
136  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
137 
138  QWaitCondition authRequestBufferNotEmpty;
139  QMutex waitConditionMutex;
140 
141  bool threadFinished = false;
142  bool success = false;
143 
144  const bool requestMadeFromMainThread = QThread::currentThread() == QApplication::instance()->thread();
145 
146  if ( mFeedback )
147  connect( mFeedback, &QgsFeedback::canceled, this, &QgsBlockingNetworkRequest::abort );
148 
149  std::function<void()> downloaderFunction = [ this, request, &waitConditionMutex, &authRequestBufferNotEmpty, &threadFinished, &success, requestMadeFromMainThread ]()
150  {
151  // this function will always be run in worker threads -- either the blocking call is being made in a worker thread,
152  // or the blocking call has been made from the main thread and we've fired up a new thread for this function
153  Q_ASSERT( QThread::currentThread() != QgsApplication::instance()->thread() );
154 
155  QgsNetworkAccessManager::instance( Qt::DirectConnection );
156 
157  success = true;
158 
159  sendRequestToNetworkAccessManager( request );
160 
161  if ( mFeedback )
162  connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
163 
164  if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
165  {
166  mErrorCode = NetworkError;
167  mErrorMessage = errorMessageFailedAuth();
168  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
169  if ( requestMadeFromMainThread )
170  authRequestBufferNotEmpty.wakeAll();
171  success = false;
172  }
173  else
174  {
175  // We are able to use direct connection here, because we
176  // * either run on the thread mReply lives in, so DirectConnection is standard and safe anyway
177  // * or the owner thread of mReply is currently not doing anything because it's blocked in future.waitForFinished() (if it is the main thread)
178  connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
179  connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
180 
181  auto resumeMainThread = [&waitConditionMutex, &authRequestBufferNotEmpty ]()
182  {
183  // when this method is called we have "produced" a single authentication request -- so the buffer is now full
184  // and it's time for the "consumer" (main thread) to do its part
185  waitConditionMutex.lock();
186  authRequestBufferNotEmpty.wakeAll();
187  waitConditionMutex.unlock();
188 
189  // note that we don't need to handle waking this thread back up - that's done automatically by QgsNetworkAccessManager
190  };
191 
192  if ( requestMadeFromMainThread )
193  {
194  connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::authRequestOccurred, this, resumeMainThread, Qt::DirectConnection );
195  connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::proxyAuthenticationRequired, this, resumeMainThread, Qt::DirectConnection );
196 
197 #ifndef QT_NO_SSL
198  connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::sslErrorsOccurred, this, resumeMainThread, Qt::DirectConnection );
199 #endif
200  }
201  QEventLoop loop;
202  // connecting to aboutToQuit avoids an on-going request to remain stalled
203  // when QThreadPool::globalInstance()->waitForDone()
204  // is called at process termination
205  connect( qApp, &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit, Qt::DirectConnection );
206  connect( this, &QgsBlockingNetworkRequest::downloadFinished, &loop, &QEventLoop::quit, Qt::DirectConnection );
207  loop.exec();
208  }
209 
210  if ( requestMadeFromMainThread )
211  {
212  waitConditionMutex.lock();
213  threadFinished = true;
214  authRequestBufferNotEmpty.wakeAll();
215  waitConditionMutex.unlock();
216  }
217  };
218 
219  if ( requestMadeFromMainThread )
220  {
221  std::unique_ptr<DownloaderThread> downloaderThread = qgis::make_unique<DownloaderThread>( downloaderFunction );
222  downloaderThread->start();
223 
224  while ( true )
225  {
226  waitConditionMutex.lock();
227  if ( threadFinished )
228  {
229  waitConditionMutex.unlock();
230  break;
231  }
232  authRequestBufferNotEmpty.wait( &waitConditionMutex );
233 
234  // If the downloader thread wakes us (the main thread) up and is not yet finished
235  // then it has "produced" an authentication request which we need to now "consume".
236  // The processEvents() call gives the auth manager the chance to show a dialog and
237  // once done with that, we can wake the downloaderThread again and continue the download.
238  if ( !threadFinished )
239  {
240  waitConditionMutex.unlock();
241 
242  QgsApplication::instance()->processEvents();
243  // we don't need to wake up the worker thread - it will automatically be woken when
244  // the auth request has been dealt with by QgsNetworkAccessManager
245  }
246  else
247  {
248  waitConditionMutex.unlock();
249  }
250  }
251  // wait for thread to gracefully exit
252  downloaderThread->wait();
253  }
254  else
255  {
256  downloaderFunction();
257  }
258  return mErrorCode;
259 }
260 
262 {
263  mIsAborted = true;
264  if ( mReply )
265  {
266  mReply->deleteLater();
267  mReply = nullptr;
268  }
269 }
270 
271 void QgsBlockingNetworkRequest::replyProgress( qint64 bytesReceived, qint64 bytesTotal )
272 {
273  QgsDebugMsgLevel( QStringLiteral( "%1 of %2 bytes downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QStringLiteral( "unknown number of" ) : QString::number( bytesTotal ) ), 2 );
274 
275  if ( bytesReceived != 0 )
276  mGotNonEmptyResponse = true;
277 
278  if ( !mIsAborted && mReply && ( !mFeedback || !mFeedback->isCanceled() ) )
279  {
280  if ( mReply->error() == QNetworkReply::NoError )
281  {
282  QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
283  if ( !redirect.isNull() )
284  {
285  // We don't want to emit downloadProgress() for a redirect
286  return;
287  }
288  }
289  }
290 
291  emit downloadProgress( bytesReceived, bytesTotal );
292 }
293 
294 void QgsBlockingNetworkRequest::replyFinished()
295 {
296  if ( !mIsAborted && mReply )
297  {
298  if ( mReply->error() == QNetworkReply::NoError && ( !mFeedback || !mFeedback->isCanceled() ) )
299  {
300  QgsDebugMsgLevel( QStringLiteral( "reply OK" ), 2 );
301  QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
302  if ( !redirect.isNull() )
303  {
304  QgsDebugMsgLevel( QStringLiteral( "Request redirected." ), 2 );
305 
306  const QUrl &toUrl = redirect.toUrl();
307  mReply->request();
308  if ( toUrl == mReply->url() )
309  {
310  mErrorMessage = tr( "Redirect loop detected: %1" ).arg( toUrl.toString() );
311  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
312  mReplyContent.clear();
313  }
314  else
315  {
316  QNetworkRequest request( toUrl );
317 
318  if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
319  {
320  mReplyContent.clear();
321  mErrorMessage = errorMessageFailedAuth();
322  mErrorCode = NetworkError;
323  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
324  emit downloadFinished();
325  return;
326  }
327 
328  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, mForceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
329  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
330 
331  mReply->deleteLater();
332  mReply = nullptr;
333 
334  QgsDebugMsgLevel( QStringLiteral( "redirected: %1 forceRefresh=%2" ).arg( redirect.toString() ).arg( mForceRefresh ), 2 );
335 
336  sendRequestToNetworkAccessManager( request );
337 
338  if ( mFeedback )
339  connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
340 
341  if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
342  {
343  mReplyContent.clear();
344  mErrorMessage = errorMessageFailedAuth();
345  mErrorCode = NetworkError;
346  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
347  emit downloadFinished();
348  return;
349  }
350 
351  connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
352  connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
353  return;
354  }
355  }
356  else
357  {
359 
360  if ( nam->cache() )
361  {
362  QNetworkCacheMetaData cmd = nam->cache()->metaData( mReply->request().url() );
363 
364  QNetworkCacheMetaData::RawHeaderList hl;
365  const auto constRawHeaders = cmd.rawHeaders();
366  for ( const QNetworkCacheMetaData::RawHeader &h : constRawHeaders )
367  {
368  if ( h.first != "Cache-Control" )
369  hl.append( h );
370  }
371  cmd.setRawHeaders( hl );
372 
373  QgsDebugMsgLevel( QStringLiteral( "expirationDate:%1" ).arg( cmd.expirationDate().toString() ), 2 );
374  if ( cmd.expirationDate().isNull() )
375  {
376  cmd.setExpirationDate( QDateTime::currentDateTime().addSecs( mExpirationSec ) );
377  }
378 
379  nam->cache()->updateMetaData( cmd );
380  }
381  else
382  {
383  QgsDebugMsgLevel( QStringLiteral( "No cache!" ), 2 );
384  }
385 
386 #ifdef QGISDEBUG
387  bool fromCache = mReply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ).toBool();
388  QgsDebugMsgLevel( QStringLiteral( "Reply was cached: %1" ).arg( fromCache ), 2 );
389 #endif
390 
391  mReplyContent = QgsNetworkReplyContent( mReply );
392  const QByteArray content = mReply->readAll();
393  if ( content.isEmpty() && !mGotNonEmptyResponse && mMethod == Get )
394  {
395  mErrorMessage = tr( "empty response: %1" ).arg( mReply->errorString() );
396  mErrorCode = ServerExceptionError;
397  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
398  }
399  mReplyContent.setContent( content );
400  }
401  }
402  else
403  {
404  if ( mReply->error() != QNetworkReply::OperationCanceledError )
405  {
406  mErrorMessage = mReply->errorString();
407  mErrorCode = ServerExceptionError;
408  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
409  }
410  mReplyContent = QgsNetworkReplyContent( mReply );
411  }
412  }
413  if ( mTimedout )
414  mErrorCode = TimeoutError;
415 
416  if ( mReply )
417  {
418  mReply->deleteLater();
419  mReply = nullptr;
420  }
421 
422  emit downloadFinished();
423 }
424 
425 QString QgsBlockingNetworkRequest::errorMessageFailedAuth()
426 {
427  return tr( "network request update failed for authentication config" );
428 }
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 head(QNetworkRequest &request, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "head" operation on the specified request.
void abort()
Aborts the network request immediately.
ErrorCode put(QNetworkRequest &request, const QByteArray &data, QgsFeedback *feedback=nullptr)
Performs a "put" operation on the specified request, using the given data.
void downloadFinished()
Emitted once a request has finished downloading.
ErrorCode deleteResource(QNetworkRequest &request, QgsFeedback *feedback=nullptr)
Performs a "delete" operation on the specified request.
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.
ErrorCode post(QNetworkRequest &request, const QByteArray &data, bool forceRefresh=false, QgsFeedback *feedback=nullptr)
Performs a "post" operation on the specified request, using the given data.
@ 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() or post() request has been made.
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::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.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39