QGIS API Documentation 3.41.0-Master (af5edcb665c)
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::CacheLoadControlAttribute, forceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
156 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
157
158 QWaitCondition authRequestBufferNotEmpty;
159 QMutex waitConditionMutex;
160
161 bool threadFinished = false;
162 bool success = false;
163
164 const bool requestMadeFromMainThread = QThread::currentThread() == QApplication::instance()->thread();
165
166 if ( mFeedback )
167 connect( mFeedback, &QgsFeedback::canceled, this, &QgsBlockingNetworkRequest::abort );
168
169 const std::function<void()> downloaderFunction = [ this, request, &waitConditionMutex, &authRequestBufferNotEmpty, &threadFinished, &success, requestMadeFromMainThread ]()
170 {
171 // this function will always be run in worker threads -- either the blocking call is being made in a worker thread,
172 // or the blocking call has been made from the main thread and we've fired up a new thread for this function
173 Q_ASSERT( QThread::currentThread() != QgsApplication::instance()->thread() );
174
175 QgsNetworkAccessManager::instance( Qt::DirectConnection );
176
177 success = true;
178
179 sendRequestToNetworkAccessManager( request );
180
181 if ( mFeedback )
182 connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
183
184 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
185 {
186 mErrorCode = NetworkError;
187 mErrorMessage = errorMessageFailedAuth();
188 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
189 if ( requestMadeFromMainThread )
190 authRequestBufferNotEmpty.wakeAll();
191 success = false;
192 }
193 else
194 {
195 // We are able to use direct connection here, because we
196 // * either run on the thread mReply lives in, so DirectConnection is standard and safe anyway
197 // * or the owner thread of mReply is currently not doing anything because it's blocked in future.waitForFinished() (if it is the main thread)
198 connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
199 connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
200 connect( mReply, &QNetworkReply::uploadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
201
202 if ( request.hasRawHeader( "Range" ) )
203 connect( mReply, &QNetworkReply::metaDataChanged, this, &QgsBlockingNetworkRequest::abortIfNotPartialContentReturned, Qt::DirectConnection );
204
205 auto resumeMainThread = [&waitConditionMutex, &authRequestBufferNotEmpty ]()
206 {
207 // when this method is called we have "produced" a single authentication request -- so the buffer is now full
208 // and it's time for the "consumer" (main thread) to do its part
209 waitConditionMutex.lock();
210 authRequestBufferNotEmpty.wakeAll();
211 waitConditionMutex.unlock();
212
213 // note that we don't need to handle waking this thread back up - that's done automatically by QgsNetworkAccessManager
214 };
215
216 if ( requestMadeFromMainThread )
217 {
218 connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::authRequestOccurred, this, resumeMainThread, Qt::DirectConnection );
219 connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::proxyAuthenticationRequired, this, resumeMainThread, Qt::DirectConnection );
220
221#ifndef QT_NO_SSL
222 connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::sslErrorsOccurred, this, resumeMainThread, Qt::DirectConnection );
223#endif
224 }
225 QEventLoop loop;
226 // connecting to aboutToQuit avoids an on-going request to remain stalled
227 // when QThreadPool::globalInstance()->waitForDone()
228 // is called at process termination
229 connect( qApp, &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit, Qt::DirectConnection );
230 connect( this, &QgsBlockingNetworkRequest::finished, &loop, &QEventLoop::quit, Qt::DirectConnection );
231 loop.exec();
232 }
233
234 if ( requestMadeFromMainThread )
235 {
236 waitConditionMutex.lock();
237 threadFinished = true;
238 authRequestBufferNotEmpty.wakeAll();
239 waitConditionMutex.unlock();
240 }
241 };
242
243 if ( requestMadeFromMainThread )
244 {
245 std::unique_ptr<DownloaderThread> downloaderThread = std::make_unique<DownloaderThread>( downloaderFunction );
246 downloaderThread->start();
247
248 while ( true )
249 {
250 waitConditionMutex.lock();
251 if ( threadFinished )
252 {
253 waitConditionMutex.unlock();
254 break;
255 }
256 authRequestBufferNotEmpty.wait( &waitConditionMutex );
257
258 // If the downloader thread wakes us (the main thread) up and is not yet finished
259 // then it has "produced" an authentication request which we need to now "consume".
260 // The processEvents() call gives the auth manager the chance to show a dialog and
261 // once done with that, we can wake the downloaderThread again and continue the download.
262 if ( !threadFinished )
263 {
264 waitConditionMutex.unlock();
265
266 QgsApplication::processEvents();
267 // we don't need to wake up the worker thread - it will automatically be woken when
268 // the auth request has been dealt with by QgsNetworkAccessManager
269 }
270 else
271 {
272 waitConditionMutex.unlock();
273 }
274 }
275 // wait for thread to gracefully exit
276 downloaderThread->wait();
277 }
278 else
279 {
280 downloaderFunction();
281 }
282 return mErrorCode;
283}
284
286{
287 mIsAborted = true;
288 if ( mReply )
289 {
290 mReply->deleteLater();
291 mReply = nullptr;
292 }
293}
294
295void QgsBlockingNetworkRequest::replyProgress( qint64 bytesReceived, qint64 bytesTotal )
296{
297 QgsDebugMsgLevel( QStringLiteral( "%1 of %2 bytes downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QStringLiteral( "unknown number of" ) : QString::number( bytesTotal ) ), 2 );
298
299 if ( bytesReceived != 0 )
300 mGotNonEmptyResponse = true;
301
302 if ( !mIsAborted && mReply && ( !mFeedback || !mFeedback->isCanceled() ) )
303 {
304 if ( mReply->error() == QNetworkReply::NoError )
305 {
306 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
307 if ( !QgsVariantUtils::isNull( redirect ) )
308 {
309 // We don't want to emit downloadProgress() for a redirect
310 return;
311 }
312 }
313 }
314
315 if ( mMethod == Put || mMethod == Post )
316 emit uploadProgress( bytesReceived, bytesTotal );
317 else
318 emit downloadProgress( bytesReceived, bytesTotal );
319}
320
321void QgsBlockingNetworkRequest::replyFinished()
322{
323 if ( !mIsAborted && mReply )
324 {
325
326 if ( mReply->error() == QNetworkReply::NoError && ( !mFeedback || !mFeedback->isCanceled() ) )
327 {
328 QgsDebugMsgLevel( QStringLiteral( "reply OK" ), 2 );
329 const QVariant redirect = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
330 if ( !QgsVariantUtils::isNull( redirect ) )
331 {
332 QgsDebugMsgLevel( QStringLiteral( "Request redirected." ), 2 );
333
334 const QUrl &toUrl = redirect.toUrl();
335 mReply->request();
336 if ( toUrl == mReply->url() )
337 {
338 mErrorMessage = tr( "Redirect loop detected: %1" ).arg( toUrl.toString() );
339 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
340 mReplyContent.clear();
341 }
342 else
343 {
344 QNetworkRequest request( toUrl );
345
346 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkRequest( request, mAuthCfg ) )
347 {
348 mReplyContent.clear();
349 mErrorMessage = errorMessageFailedAuth();
350 mErrorCode = NetworkError;
351 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
352 emit finished();
354 emit downloadFinished();
356 return;
357 }
358
359 request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, mForceRefresh ? QNetworkRequest::AlwaysNetwork : QNetworkRequest::PreferCache );
360 request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
361
362 // if that was a range request, use the same range for the redirected request
363 if ( mReply->request().hasRawHeader( "Range" ) )
364 request.setRawHeader( "Range", mReply->request().rawHeader( "Range" ) );
365
366 mReply->deleteLater();
367 mReply = nullptr;
368
369 QgsDebugMsgLevel( QStringLiteral( "redirected: %1 forceRefresh=%2" ).arg( redirect.toString() ).arg( mForceRefresh ), 2 );
370
371 sendRequestToNetworkAccessManager( request );
372
373 if ( mFeedback )
374 connect( mFeedback, &QgsFeedback::canceled, mReply, &QNetworkReply::abort );
375
376 if ( !mAuthCfg.isEmpty() && !QgsApplication::authManager()->updateNetworkReply( mReply, mAuthCfg ) )
377 {
378 mReplyContent.clear();
379 mErrorMessage = errorMessageFailedAuth();
380 mErrorCode = NetworkError;
381 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
382 emit finished();
384 emit downloadFinished();
386 return;
387 }
388
389 connect( mReply, &QNetworkReply::finished, this, &QgsBlockingNetworkRequest::replyFinished, Qt::DirectConnection );
390 connect( mReply, &QNetworkReply::downloadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
391 connect( mReply, &QNetworkReply::uploadProgress, this, &QgsBlockingNetworkRequest::replyProgress, Qt::DirectConnection );
392
393 if ( request.hasRawHeader( "Range" ) )
394 connect( mReply, &QNetworkReply::metaDataChanged, this, &QgsBlockingNetworkRequest::abortIfNotPartialContentReturned, Qt::DirectConnection );
395
396 return;
397 }
398 }
399 else
400 {
402
403 if ( nam->cache() )
404 {
405 QNetworkCacheMetaData cmd = nam->cache()->metaData( mReply->request().url() );
406
407 QNetworkCacheMetaData::RawHeaderList hl;
408 const auto constRawHeaders = cmd.rawHeaders();
409 for ( const QNetworkCacheMetaData::RawHeader &h : constRawHeaders )
410 {
411 if ( h.first != "Cache-Control" )
412 hl.append( h );
413 }
414 cmd.setRawHeaders( hl );
415
416 QgsDebugMsgLevel( QStringLiteral( "expirationDate:%1" ).arg( cmd.expirationDate().toString() ), 2 );
417 if ( cmd.expirationDate().isNull() )
418 {
419 cmd.setExpirationDate( QDateTime::currentDateTime().addSecs( mExpirationSec ) );
420 }
421
422 nam->cache()->updateMetaData( cmd );
423 }
424 else
425 {
426 QgsDebugMsgLevel( QStringLiteral( "No cache!" ), 2 );
427 }
428
429#ifdef QGISDEBUG
430 const bool fromCache = mReply->attribute( QNetworkRequest::SourceIsFromCacheAttribute ).toBool();
431 QgsDebugMsgLevel( QStringLiteral( "Reply was cached: %1" ).arg( fromCache ), 2 );
432#endif
433
434 mReplyContent = QgsNetworkReplyContent( mReply );
435 const QByteArray content = mReply->readAll();
436 if ( !( mRequestFlags & RequestFlag::EmptyResponseIsValid ) && content.isEmpty() && !mGotNonEmptyResponse && mMethod == Get )
437 {
438 mErrorMessage = tr( "empty response: %1" ).arg( mReply->errorString() );
439 mErrorCode = ServerExceptionError;
440 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
441 }
442 mReplyContent.setContent( content );
443 }
444 }
445 else
446 {
447 if ( mReply->error() != QNetworkReply::OperationCanceledError )
448 {
449 mErrorMessage = mReply->errorString();
450 mErrorCode = ServerExceptionError;
451 QgsMessageLog::logMessage( mErrorMessage, tr( "Network" ) );
452 }
453 mReplyContent = QgsNetworkReplyContent( mReply );
454 mReplyContent.setContent( mReply->readAll() );
455 }
456 }
457 if ( mTimedout )
458 mErrorCode = TimeoutError;
459
460 if ( mReply )
461 {
462 mReply->deleteLater();
463 mReply = nullptr;
464 }
465
466 emit finished();
468 emit downloadFinished();
470}
471
472QString QgsBlockingNetworkRequest::errorMessageFailedAuth()
473{
474 return tr( "network request update failed for authentication config" );
475}
476
477void QgsBlockingNetworkRequest::abortIfNotPartialContentReturned()
478{
479 if ( mReply && mReply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt() == 200 )
480 {
481 // We're expecting a 206 - Partial Content but the server returned 200
482 // It seems it does not support range requests and is returning the whole file!
483 mReply->abort();
484 mErrorMessage = tr( "The server does not support range requests" );
485 mErrorCode = ServerExceptionError;
486 }
487}
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)
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:6643
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:6642
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:39