LCOV - code coverage report
Current view: top level - src/network - ResilientNetworkReply.cpp (source / functions) Coverage Total Hit
Test: coverage.info.cleaned Lines: 62.6 % 171 107
Test Date: 2026-01-27 22:31:25 Functions: 61.1 % 18 11

            Line data    Source code
       1              : #include "ResilientNetworkReply.h"
       2              : #include "FangNetworkAccessManager.h"
       3              : #include "../utilities/FangLogging.h"
       4              : #include <QDebug>
       5              : 
       6              : static const int DEFAULT_TIMEOUT_MS = 30000; // 30 seconds
       7              : 
       8           32 : ResilientNetworkReply::ResilientNetworkReply(FangNetworkAccessManager* manager,
       9              :                                              const QNetworkRequest& request,
      10              :                                              QNetworkAccessManager::Operation operation,
      11              :                                              const NetworkRetryPolicy& policy,
      12           32 :                                              QObject* parent)
      13              :     : QObject(parent)
      14           32 :     , networkManager(manager)
      15           32 :     , networkRequest(request)
      16           32 :     , operation(operation)
      17           32 :     , retryPolicy(policy)
      18           32 :     , currentReply(nullptr)
      19           32 :     , currentAttemptCount(0)
      20           32 :     , customTimeout(-1)
      21           32 :     , lastError(QNetworkReply::NoError)
      22           32 :     , isSuccessful(false)
      23           32 :     , fromCache(false)
      24           32 :     , aborted(false)
      25              : {
      26           32 :     timeoutTimer.setSingleShot(true);
      27           32 :     connect(&timeoutTimer, &QTimer::timeout, this, &ResilientNetworkReply::onTimeout);
      28              : 
      29           32 :     retryTimer.setSingleShot(true);
      30           32 :     connect(&retryTimer, &QTimer::timeout, this, &ResilientNetworkReply::onRetryTimerTimeout);
      31           32 : }
      32              : 
      33           64 : ResilientNetworkReply::~ResilientNetworkReply()
      34              : {
      35           32 :     cleanup();
      36           64 : }
      37              : 
      38           32 : void ResilientNetworkReply::start()
      39              : {
      40           32 :     if (aborted) {
      41            0 :         qCWarning(logNetwork) << "Cannot start aborted request";
      42            0 :         return;
      43              :     }
      44              : 
      45           32 :     currentAttemptCount = 0;
      46           32 :     errorHistory.clear();
      47           32 :     accumulatedData.clear();
      48           32 :     executeRequest();
      49              : }
      50              : 
      51            0 : void ResilientNetworkReply::abort()
      52              : {
      53            0 :     aborted = true;
      54            0 :     retryTimer.stop();
      55            0 :     cleanup();
      56            0 : }
      57              : 
      58            0 : void ResilientNetworkReply::setTimeout(int timeout)
      59              : {
      60            0 :     customTimeout = timeout;
      61            0 : }
      62              : 
      63           45 : void ResilientNetworkReply::executeRequest()
      64              : {
      65           45 :     if (aborted) {
      66            0 :         return;
      67              :     }
      68              : 
      69           45 :     currentAttemptCount++;
      70           45 :     requestTimer.start();
      71              : 
      72          135 :     qCDebug(logNetwork) << "Network request attempt" << currentAttemptCount
      73           90 :                         << "for" << networkRequest.url();
      74              : 
      75              :     // Clean up previous reply if exists
      76           45 :     cleanup();
      77              : 
      78              :     // Create new network request
      79           45 :     switch (operation) {
      80           45 :     case QNetworkAccessManager::GetOperation:
      81           45 :         currentReply = networkManager->get(networkRequest);
      82           45 :         break;
      83            0 :     case QNetworkAccessManager::PostOperation:
      84              :         // For POST, would need to handle body data
      85            0 :         qCWarning(logNetwork) << "POST operation not yet implemented in ResilientNetworkReply";
      86            0 :         currentReply = networkManager->get(networkRequest);
      87            0 :         break;
      88            0 :     case QNetworkAccessManager::HeadOperation:
      89            0 :         currentReply = networkManager->head(networkRequest);
      90            0 :         break;
      91            0 :     default:
      92            0 :         qCWarning(logNetwork) << "Unsupported network operation:" << operation;
      93            0 :         currentReply = networkManager->get(networkRequest);
      94            0 :         break;
      95              :     }
      96              : 
      97           45 :     if (!currentReply) {
      98            0 :         qCCritical(logNetwork) << "Failed to create network reply";
      99            0 :         emit failed(QNetworkReply::UnknownNetworkError);
     100            0 :         return;
     101              :     }
     102              : 
     103              :     // Connect signals
     104           45 :     connect(currentReply, &QNetworkReply::finished,
     105           45 :             this, &ResilientNetworkReply::onReplyFinished);
     106           45 :     connect(currentReply, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::errorOccurred),
     107           45 :             this, &ResilientNetworkReply::onReplyError);
     108           45 :     connect(currentReply, &QNetworkReply::downloadProgress,
     109           45 :             this, &ResilientNetworkReply::onDownloadProgress);
     110              : 
     111              :     // Start timeout.
     112           45 :     int timeout = (customTimeout > 0) ? customTimeout : DEFAULT_TIMEOUT_MS;
     113           45 :     timeoutTimer.start(timeout);
     114              : 
     115              :     // Check if cached.
     116           45 :     QVariant fromCache = currentReply->attribute(QNetworkRequest::SourceIsFromCacheAttribute);
     117           45 :     fromCache = fromCache.isValid() && fromCache.toBool();
     118              : }
     119              : 
     120           45 : void ResilientNetworkReply::onReplyFinished()
     121              : {
     122           45 :     timeoutTimer.stop();
     123              : 
     124           45 :     if (aborted || !currentReply) {
     125            0 :         return;
     126              :     }
     127              : 
     128           45 :     qint64 elapsed = requestTimer.elapsed();
     129              : 
     130              :     // Check for errors
     131           45 :     QNetworkReply::NetworkError error = currentReply->error();
     132              : 
     133           45 :     if (error == QNetworkReply::NoError) {
     134              :         // Yay! Success!
     135           21 :         accumulatedData = currentReply->readAll();
     136           21 :         isSuccessful = true;
     137           21 :         lastError = QNetworkReply::NoError;
     138              : 
     139           42 :         qCDebug(logNetwork) << "Network request succeeded for" << networkRequest.url()
     140           21 :                             << "in" << elapsed << "ms"
     141           21 :                             << "(" << accumulatedData.size() << "bytes)"
     142           21 :                             << "after" << currentAttemptCount << "attempts";
     143              : 
     144           21 :         emit finished();
     145              :     } else {
     146              :         // Error occurred :(
     147           24 :         onReplyError(error);
     148              :     }
     149              : }
     150              : 
     151           24 : void ResilientNetworkReply::onReplyError(QNetworkReply::NetworkError error)
     152              : {
     153           24 :     timeoutTimer.stop();
     154              : 
     155           24 :     if (aborted) {
     156            0 :         return;
     157              :     }
     158              : 
     159           24 :     lastError = error;
     160           24 :     lastErrorString = currentReply ? currentReply->errorString() : "Unknown error";
     161              : 
     162              :     // Record error.
     163           24 :     QString errorDesc = QString("Attempt %1: %2 (%3)")
     164           48 :                             .arg(currentAttemptCount)
     165           48 :                             .arg(QVariant::fromValue(error).toString())
     166           24 :                             .arg(lastErrorString);
     167           24 :     errorHistory.append(qMakePair(currentAttemptCount, errorDesc));
     168              : 
     169           48 :     qCWarning(logNetwork) << "Network error for"  << networkRequest.url()
     170           24 :                           << ":" << errorDesc;
     171              : 
     172              :     // Check if it's worth retrying.
     173           24 :     if (retryPolicy.shouldRetry(currentAttemptCount) && retryPolicy.isRetryable(error)) {
     174           13 :         scheduleRetry();
     175              :     } else {
     176              :         // No more retries or error not retryable
     177           33 :         qCWarning(logNetwork) << "Request failed after " << currentAttemptCount << " attempts:"
     178           22 :                               << networkRequest.url();
     179           22 :         qCWarning(logNetwork) << "Error history: ";
     180           26 :         for (const auto& entry : errorHistory) {
     181           30 :             qCWarning(logNetwork) << "  " << entry.second;
     182              :         }
     183              : 
     184           11 :         emit failed(error);
     185              :     }
     186           24 : }
     187              : 
     188            0 : void ResilientNetworkReply::onTimeout()
     189              : {
     190            0 :     if (aborted) {
     191            0 :         return;
     192              :     }
     193              : 
     194            0 :     qCWarning(logNetwork) << "Network timeout for" << networkRequest.url()
     195            0 :                           << "after" << requestTimer.elapsed() << "ms";
     196              : 
     197              :     // Record timeout.
     198            0 :     errorHistory.append(qMakePair(currentAttemptCount,
     199            0 :                                    QString("Attempt %1: Timeout").arg(currentAttemptCount)));
     200              : 
     201              :     // Abort!
     202            0 :     if (currentReply) {
     203            0 :         currentReply->abort();
     204              :     }
     205              : 
     206            0 :     lastError = QNetworkReply::TimeoutError;
     207            0 :     lastErrorString = "Request timed out";
     208              : 
     209            0 :     emit timeout();
     210              : 
     211              :     // Check if should retry.
     212            0 :     if (retryPolicy.shouldRetry(currentAttemptCount) && retryPolicy.isRetryable(QNetworkReply::TimeoutError)) {
     213            0 :         scheduleRetry();
     214              :     } else {
     215            0 :         qCWarning(logNetwork) << "Request timed out after " << currentAttemptCount << " attempts";
     216            0 :         emit failed(QNetworkReply::TimeoutError);
     217              :     }
     218              : }
     219              : 
     220           13 : void ResilientNetworkReply::scheduleRetry()
     221              : {
     222           13 :     int delay = retryPolicy.calculateDelay(currentAttemptCount);
     223              : 
     224           39 :     qCInfo(logNetwork) << "Scheduling retry " << (currentAttemptCount + 1)
     225           39 :                        << "for " << networkRequest.url()
     226           13 :                        << "in " << delay << " ms";
     227              : 
     228           13 :     emit retrying(currentAttemptCount + 1, delay);
     229              : 
     230           13 :     retryTimer.start(delay);
     231           13 : }
     232              : 
     233           13 : void ResilientNetworkReply::onRetryTimerTimeout()
     234              : {
     235           13 :     if (aborted) {
     236            0 :         return;
     237              :     }
     238              : 
     239           13 :     executeRequest();
     240              : }
     241              : 
     242            0 : void ResilientNetworkReply::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
     243              : {
     244              :     // Reset timeout if the download is progressing slowly (slow connection, large file, etc.)
     245            0 :     if (bytesReceived > 0) {
     246            0 :         int timeout = (customTimeout > 0) ? customTimeout : DEFAULT_TIMEOUT_MS;
     247            0 :         timeoutTimer.start(timeout);
     248              :     }
     249              : 
     250            0 :     emit downloadProgress(bytesReceived, bytesTotal);
     251            0 : }
     252              : 
     253           77 : void ResilientNetworkReply::cleanup()
     254              : {
     255           77 :     timeoutTimer.stop();
     256              : 
     257           77 :     if (currentReply) {
     258           45 :         currentReply->disconnect(this);
     259           45 :         if (currentReply->isRunning()) {
     260           45 :             currentReply->abort();
     261              :         }
     262           45 :         currentReply->deleteLater();
     263           45 :         currentReply = nullptr;
     264              :     }
     265           77 : }
     266              : 
     267            0 : QUrl ResilientNetworkReply::url() const
     268              : {
     269            0 :     if (currentReply) {
     270            0 :         return currentReply->url();
     271              :     }
     272            0 :     return networkRequest.url();
     273              : }
     274              : 
     275            9 : QByteArray ResilientNetworkReply::readAll()
     276              : {
     277            9 :     return accumulatedData;
     278              : }
     279              : 
     280            0 : QString ResilientNetworkReply::errorString() const
     281              : {
     282            0 :     if (!lastErrorString.isEmpty()) {
     283            0 :         return lastErrorString;
     284              :     }
     285            0 :     if (currentReply) {
     286            0 :         return currentReply->errorString();
     287              :     }
     288            0 :     return QString();
     289              : }
     290              : 
     291            0 : int ResilientNetworkReply::httpStatusCode() const
     292              : {
     293            0 :     if (currentReply) {
     294            0 :         return currentReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
     295              :     }
     296            0 :     return 0;
     297              : }
        

Generated by: LCOV version 2.0-1