QGIS API Documentation  3.20.0-Odense (decaadbb31)
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(), qOverload< QNetworkReply * >( &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 = std::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 
299  if ( mReply->error() == QNetworkReply::NoError && ( !mFeedback || !mFeedback->isCanceled() ) )
300  {
301  QgsDebugMsgLevel( QStringLiteral( "reply OK" ), 2 );
302  QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
303  if ( !redirect.isNull() )
304  {
305  QgsDebugMsgLevel( QStringLiteral( "Request redirected." ), 2 );
306 
307  const QUrl &toUrl = redirect.toUrl();
308  mReply->request();
309  if ( toUrl == mReply->url() )
310  {
311  mErrorMessage = tr( "Redirect loop detected: %1" ).arg( toUrl.toString() );
312  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
313  mReplyContent.clear();
314  }
315  else
316  {
317  QNetworkRequest request( toUrl );
318 
319  if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
320  {
321  mReplyContent.clear();
322  mErrorMessage = errorMessageFailedAuth();
323  mErrorCode = NetworkError;
324  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
325  emit downloadFinished();
326  return;
327  }
328 
329  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, mForceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
330  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
331 
332  mReply->deleteLater();
333  mReply = nullptr;
334 
335  QgsDebugMsgLevel( QStringLiteral( "redirected: %1 forceRefresh=%2" ).arg( redirect.toString() ).arg( mForceRefresh ), 2 );
336 
337  sendRequestToNetworkAccessManager( request );
338 
339  if ( mFeedback )
340  connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
341 
342  if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
343  {
344  mReplyContent.clear();
345  mErrorMessage = errorMessageFailedAuth();
346  mErrorCode = NetworkError;
347  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
348  emit downloadFinished();
349  return;
350  }
351 
352  connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
353  connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
354  return;
355  }
356  }
357  else
358  {
360 
361  if ( nam->cache() )
362  {
363  QNetworkCacheMetaData cmd = nam->cache()->metaData( mReply->request().url() );
364 
365  QNetworkCacheMetaData::RawHeaderList hl;
366  const auto constRawHeaders = cmd.rawHeaders();
367  for ( const QNetworkCacheMetaData::RawHeader &h : constRawHeaders )
368  {
369  if ( h.first != "Cache-Control" )
370  hl.append( h );
371  }
372  cmd.setRawHeaders( hl );
373 
374  QgsDebugMsgLevel( QStringLiteral( "expirationDate:%1" ).arg( cmd.expirationDate().toString() ), 2 );
375  if ( cmd.expirationDate().isNull() )
376  {
377  cmd.setExpirationDate( QDateTime::currentDateTime().addSecs( mExpirationSec ) );
378  }
379 
380  nam->cache()->updateMetaData( cmd );
381  }
382  else
383  {
384  QgsDebugMsgLevel( QStringLiteral( "No cache!" ), 2 );
385  }
386 
387 #ifdef QGISDEBUG
388  bool fromCache = mReply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ).toBool();
389  QgsDebugMsgLevel( QStringLiteral( "Reply was cached: %1" ).arg( fromCache ), 2 );
390 #endif
391 
392  mReplyContent = QgsNetworkReplyContent( mReply );
393  const QByteArray content = mReply->readAll();
394  if ( content.isEmpty() && !mGotNonEmptyResponse && mMethod == Get )
395  {
396  mErrorMessage = tr( "empty response: %1" ).arg( mReply->errorString() );
397  mErrorCode = ServerExceptionError;
398  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
399  }
400  mReplyContent.setContent( content );
401  }
402  }
403  else
404  {
405  if ( mReply->error() != QNetworkReply::OperationCanceledError )
406  {
407  mErrorMessage = mReply->errorString();
408  mErrorCode = ServerExceptionError;
409  QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
410  }
411  mReplyContent = QgsNetworkReplyContent( mReply );
412  mReplyContent.setContent( mReply->readAll() );
413  }
414  }
415  if ( mTimedout )
416  mErrorCode = TimeoutError;
417 
418  if ( mReply )
419  {
420  mReply->deleteLater();
421  mReply = nullptr;
422  }
423 
424  emit downloadFinished();
425 }
426 
427 QString QgsBlockingNetworkRequest::errorMessageFailedAuth()
428 {
429  return tr( "network request update failed for authentication config" );
430 }
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::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.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39