From e8da9ee4fb81623bdf5f174bc33ce74a5e80a517 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 2 Feb 2026 05:09:11 -0700 Subject: [PATCH] feat: Auto handle Http 429 Too Many Requests with retry - Must be explicitly enabled for a request - Uses Retry-After Header if present, falls back to exponential back off starting with 10 seconds - if retry delay is greater than 1 minute or it retries more than 3 times then fail with a "Rate Limited" reason - Sets task status to inform user of retry. Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .../minecraft/auth/steps/EntitlementsStep.cpp | 1 + launcher/minecraft/auth/steps/GetSkinStep.cpp | 1 + .../auth/steps/LauncherLoginStep.cpp | 1 + .../auth/steps/MSADeviceCodeStep.cpp | 1 + .../auth/steps/MinecraftProfileStep.cpp | 1 + .../auth/steps/XboxAuthorizationStep.cpp | 1 + .../minecraft/auth/steps/XboxUserStep.cpp | 1 + launcher/net/NetRequest.cpp | 59 ++++++++++++++++++- launcher/net/NetRequest.h | 12 +++- 9 files changed, 75 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp index 890c1d77b..35a7de66f 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.cpp +++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -34,6 +34,7 @@ void EntitlementsStep::perform() m_response.reset(new QByteArray()); m_request = Net::Download::makeByteArray(url, m_response.get()); m_request->addHeaderProxy(std::make_unique(headers)); + m_request->enableAutoRetry(true); m_task.reset(new NetJob("EntitlementsStep", APPLICATION->network())); m_task->setAskRetry(false); diff --git a/launcher/minecraft/auth/steps/GetSkinStep.cpp b/launcher/minecraft/auth/steps/GetSkinStep.cpp index ac83119e9..0843b25c6 100644 --- a/launcher/minecraft/auth/steps/GetSkinStep.cpp +++ b/launcher/minecraft/auth/steps/GetSkinStep.cpp @@ -18,6 +18,7 @@ void GetSkinStep::perform() m_response.reset(new QByteArray()); m_request = Net::Download::makeByteArray(url, m_response.get()); + m_request->enableAutoRetry(true); m_task.reset(new NetJob("GetSkinStep", APPLICATION->network())); m_task->setAskRetry(false); diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index ab4748bca..eb104f4eb 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -39,6 +39,7 @@ void LauncherLoginStep::perform() m_response.reset(new QByteArray()); m_request = Net::Upload::makeByteArray(url, m_response.get(), requestBody.toUtf8()); m_request->addHeaderProxy(std::make_unique(headers)); + m_request->enableAutoRetry(true); m_task.reset(new NetJob("LauncherLoginStep", APPLICATION->network())); m_task->setAskRetry(false); diff --git a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp index c11fcd8b6..4cec52599 100644 --- a/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp +++ b/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp @@ -69,6 +69,7 @@ void MSADeviceCodeStep::perform() m_response.reset(new QByteArray()); m_request = Net::Upload::makeByteArray(url, m_response.get(), payload); m_request->addHeaderProxy(std::make_unique(headers)); + m_request->enableAutoRetry(true); m_task.reset(new NetJob("MSADeviceCodeStep", APPLICATION->network())); m_task->setAskRetry(false); diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp index 93189c683..7331eac31 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -24,6 +24,7 @@ void MinecraftProfileStep::perform() m_response.reset(new QByteArray()); m_request = Net::Download::makeByteArray(url, m_response.get()); m_request->addHeaderProxy(std::make_unique(headers)); + m_request->enableAutoRetry(true); m_task.reset(new NetJob("MinecraftProfileStep", APPLICATION->network())); m_task->setAskRetry(false); diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp index 29c69f255..3de93bdc7 100644 --- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -45,6 +45,7 @@ void XboxAuthorizationStep::perform() m_response.reset(new QByteArray()); m_request = Net::Upload::makeByteArray(url, m_response.get(), xbox_auth_data.toUtf8()); m_request->addHeaderProxy(std::make_unique(headers)); + m_request->enableAutoRetry(true); m_task.reset(new NetJob("XboxAuthorizationStep", APPLICATION->network())); m_task->setAskRetry(false); diff --git a/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp index a48997177..c13239dd2 100644 --- a/launcher/minecraft/auth/steps/XboxUserStep.cpp +++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -40,6 +40,7 @@ void XboxUserStep::perform() m_response.reset(new QByteArray()); m_request = Net::Upload::makeByteArray(url, m_response.get(), xbox_auth_data.toUtf8()); m_request->addHeaderProxy(std::make_unique(headers)); + m_request->enableAutoRetry(true); m_task.reset(new NetJob("XboxUserStep", APPLICATION->network())); m_task->setAskRetry(false); diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp index 957b2d1e5..da909e65b 100644 --- a/launcher/net/NetRequest.cpp +++ b/launcher/net/NetRequest.cpp @@ -41,8 +41,10 @@ #include #include +#include #include #include +#include #include #if defined(LAUNCHER_APPLICATION) @@ -55,6 +57,11 @@ namespace Net { +NetRequest::NetRequest() : Task() +{ + connect(&m_retryTimer, &QTimer::timeout, this, &NetRequest::executeTask); +} + void NetRequest::addValidator(Validator* v) { m_sink->addValidator(v); @@ -161,6 +168,20 @@ void NetRequest::downloadError(QNetworkReply::NetworkError error) if (error == QNetworkReply::OperationCanceledError) { qCCritical(logCat) << getUid().toString() << "Aborted" << m_url.toString(); m_state = State::Failed; + } else if (replyStatusCode() == 429 /* HTTP Too Many Requests*/ && m_options & Option::AutoRetry) { + qCDebug(logCat) << getUid().toString() << "Rate Limited!"; + int64_t delay = 10 * std::pow(2, m_retryCount); + if (m_reply->hasRawHeader("Retry-After")) { + auto retryAfter = m_reply->rawHeader("Retry-After"); + if (retryAfter.trimmed().endsWith("GMT")) /* HTTP Date format */ { + auto afterTimestamp = QDateTime::fromString(QString::fromUtf8(retryAfter.trimmed()), "ddd, dd MMM yyyy HH:mm:ss 'GMT'"); + auto now = QDateTime::currentDateTime(); + delay = now.secsTo(afterTimestamp); + } else { + delay = retryAfter.toLong(); + } + } + handleAutoRetry(delay); } else { if (m_options & Option::AcceptLocalFiles) { if (m_sink->hasLocalData()) { @@ -182,7 +203,8 @@ void NetRequest::sslErrors(const QList& errors) { int i = 1; for (auto error : errors) { - qCCritical(logCat).nospace() << getUid().toString() << " Request " << m_url.toString() << " SSL Error #" << i << ": " << error.errorString(); + qCCritical(logCat).nospace() << getUid().toString() << " Request " << m_url.toString() << " SSL Error #" << i << ": " + << error.errorString(); auto cert = error.certificate(); qCCritical(logCat) << getUid().toString() << "Certificate in question:\n" << cert.toText(); i++; @@ -243,8 +265,33 @@ auto NetRequest::handleRedirect() -> bool return true; } +void NetRequest::handleAutoRetry(int64_t delay) +{ + m_retryCount++; + if (delay > 60 || m_retryCount > 4) { + /* 1 minute is too long to wait for retry, fail for now */ + m_state = State::Failed; + auto retryAfter = QDateTime::currentDateTime().addSecs(delay); + emitFailed(tr("Request Rate Limited for %n second(s): Retry After %1", "seconds", delay) + .arg(retryAfter.toLocalTime().toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)))); + return; + } else { + qCDebug(logCat) << getUid().toString() << "Retyring Request in" << delay << "seconds"; + setStatus(tr("Rate Limited: Waiting %n second(s)", "seconds", delay)); + m_retryTimer.setTimerType(Qt::VeryCoarseTimer); + m_retryTimer.setSingleShot(true); + m_retryTimer.setInterval(delay * 1000); + m_retryTimer.start(); + } +} + void NetRequest::downloadFinished() { + // currently waiting for retry + if (m_retryTimer.isActive()) { + return; + } + // handle HTTP redirection first if (handleRedirect()) { qCDebug(logCat) << getUid().toString() << "Request redirected:" << m_url.toString(); @@ -351,4 +398,14 @@ QString NetRequest::errorString() const { return m_reply ? m_reply->errorString() : ""; } + +void NetRequest::enableAutoRetry(bool enable) +{ + if (enable) { + m_options |= Option::AutoRetry; + } else { + m_options &= ~static_cast(Option::AutoRetry); + } +} + } // namespace Net diff --git a/launcher/net/NetRequest.h b/launcher/net/NetRequest.h index bdab9d68f..e38152b83 100644 --- a/launcher/net/NetRequest.h +++ b/launcher/net/NetRequest.h @@ -41,6 +41,7 @@ #include #include +#include #include #include "HeaderProxy.h" @@ -55,11 +56,11 @@ namespace Net { class NetRequest : public Task { Q_OBJECT protected: - explicit NetRequest() : Task() {} + explicit NetRequest(); public: using Ptr = shared_qobject_ptr; - enum class Option { NoOptions = 0, AcceptLocalFiles = 1, MakeEternal = 2 }; + enum class Option { NoOptions = 0, AcceptLocalFiles = 1, MakeEternal = 2, AutoRetry = 4 }; Q_DECLARE_FLAGS(Options, Option) public: @@ -71,6 +72,9 @@ class NetRequest : public Task { void setNetwork(QNetworkAccessManager* network) { m_network = network; } void addHeaderProxy(std::unique_ptr proxy) { m_headerProxies.push_back(std::move(proxy)); } + // automatically handle HTTP 429 Too Many Requests errors and retry + void enableAutoRetry(bool enable); + QUrl url() const; void setUrl(QUrl url) { m_url = url; } int replyStatusCode() const; @@ -79,6 +83,7 @@ class NetRequest : public Task { private: auto handleRedirect() -> bool; + void handleAutoRetry(int64_t delay); virtual QNetworkReply* getReply(QNetworkRequest&) = 0; protected slots: @@ -109,6 +114,9 @@ class NetRequest : public Task { /// source URL QUrl m_url; std::vector> m_headerProxies; + + int m_retryCount = 0; + QTimer m_retryTimer; }; } // namespace Net