From 22b290cb2d1832fdff54e0f060e4e249ed063fcb Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 1 Jul 2023 14:59:59 +0200 Subject: [PATCH] Improve network error messages (#4704) --- CHANGELOG.md | 1 + src/common/NetworkPrivate.cpp | 13 +- src/common/NetworkResult.cpp | 28 +- src/common/NetworkResult.hpp | 30 ++- src/providers/IvrApi.cpp | 4 +- src/providers/RecentMessagesApi.cpp | 9 +- src/providers/bttv/BttvEmotes.cpp | 20 +- src/providers/ffz/FfzEmotes.cpp | 21 +- src/providers/seventv/SeventvEmotes.cpp | 29 +-- src/providers/twitch/api/Helix.cpp | 325 +++++++++++++++++------- src/singletons/Updates.cpp | 8 +- src/util/NuulsUploader.cpp | 2 +- tests/CMakeLists.txt | 1 + tests/src/NetworkRequest.cpp | 8 +- tests/src/NetworkResult.cpp | 48 ++++ 15 files changed, 378 insertions(+), 169 deletions(-) create mode 100644 tests/src/NetworkResult.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b6f3b4e..0e2c14ccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Bugfix: Fix visual glitches with smooth scrolling. (#4501) - Bugfix: Fixed pings firing for the "Your username" highlight when not signed in. (#4698) - Bugfix: Fixed partially broken filters on Qt 6 builds. (#4702) +- Bugfix: Fixed some network errors having `0` as their HTTP status. (#4704) - Bugfix: Fixed crash that could occurr when closing the usercard too quickly after blocking or unblocking a user. (#4711) - Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) - Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) diff --git a/src/common/NetworkPrivate.cpp b/src/common/NetworkPrivate.cpp index 9af301883..661b2eccf 100644 --- a/src/common/NetworkPrivate.cpp +++ b/src/common/NetworkPrivate.cpp @@ -155,7 +155,8 @@ void loadUncached(std::shared_ptr &&data) { postToThread([data] { data->onError_(NetworkResult( - {}, NetworkResult::timedoutStatus)); + NetworkResult::NetworkError::TimeoutError, {}, + {})); }); } @@ -218,8 +219,9 @@ void loadUncached(std::shared_ptr &&data) QString(data->payload_)); } // TODO: Should this always be run on the GUI thread? - postToThread([data, code = status.toInt(), reply] { - data->onError_(NetworkResult(reply->readAll(), code)); + postToThread([data, status, reply] { + data->onError_(NetworkResult(reply->error(), status, + reply->readAll())); }); } @@ -238,7 +240,7 @@ void loadUncached(std::shared_ptr &&data) auto status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - NetworkResult result(bytes, status.toInt()); + NetworkResult result(reply->error(), status, bytes); DebugCount::increase("http request success"); // log("starting {}", data->request_.url().toString()); @@ -337,7 +339,8 @@ void loadCached(std::shared_ptr &&data) // XXX: check if bytes is empty? QByteArray bytes = cachedFile.readAll(); - NetworkResult result(bytes, 200); + NetworkResult result(NetworkResult::NetworkError::NoError, QVariant(200), + bytes); qCDebug(chatterinoHTTP) << QString("%1 [CACHED] 200 %2") diff --git a/src/common/NetworkResult.cpp b/src/common/NetworkResult.cpp index 0a5295fdb..ae614bac0 100644 --- a/src/common/NetworkResult.cpp +++ b/src/common/NetworkResult.cpp @@ -3,15 +3,21 @@ #include "common/QLogging.hpp" #include +#include #include #include namespace chatterino { -NetworkResult::NetworkResult(const QByteArray &data, int status) - : data_(data) - , status_(status) +NetworkResult::NetworkResult(NetworkError error, const QVariant &httpStatusCode, + QByteArray data) + : data_(std::move(data)) + , error_(error) { + if (httpStatusCode.isValid()) + { + this->status_ = httpStatusCode.toInt(); + } } QJsonObject NetworkResult::parseJson() const @@ -59,9 +65,21 @@ const QByteArray &NetworkResult::getData() const return this->data_; } -int NetworkResult::status() const +QString NetworkResult::formatError() const { - return this->status_; + if (this->status_) + { + return QString::number(*this->status_); + } + + const auto *name = + QMetaEnum::fromType().valueToKey( + this->error_); + if (name == nullptr) + { + return QStringLiteral("unknown error (%1)").arg(this->error_); + } + return name; } } // namespace chatterino diff --git a/src/common/NetworkResult.hpp b/src/common/NetworkResult.hpp index 64baed5ea..9f0ada784 100644 --- a/src/common/NetworkResult.hpp +++ b/src/common/NetworkResult.hpp @@ -2,14 +2,20 @@ #include #include +#include #include +#include + namespace chatterino { class NetworkResult { public: - NetworkResult(const QByteArray &data, int status); + using NetworkError = QNetworkReply::NetworkError; + + NetworkResult(NetworkError error, const QVariant &httpStatusCode, + QByteArray data); /// Parses the result as json and returns the root as an object. /// Returns empty object if parsing failed. @@ -20,13 +26,29 @@ public: /// Parses the result as json and returns the document. rapidjson::Document parseRapidJson() const; const QByteArray &getData() const; - int status() const; - static constexpr int timedoutStatus = -2; + /// The error code of the reply. + /// In case of a successful reply, this will be NoError (0) + NetworkError error() const + { + return this->error_; + } + + /// The HTTP status code if a response was received. + std::optional status() const + { + return this->status_; + } + + /// Formats the error. + /// If a reply is received, returns the HTTP status otherwise, the network error. + QString formatError() const; private: QByteArray data_; - int status_; + + NetworkError error_; + std::optional status_; }; } // namespace chatterino diff --git a/src/providers/IvrApi.cpp b/src/providers/IvrApi.cpp index 26b4088e5..868a9ff08 100644 --- a/src/providers/IvrApi.cpp +++ b/src/providers/IvrApi.cpp @@ -27,7 +27,7 @@ void IvrApi::getSubage(QString userName, QString channelName, }) .onError([failureCallback](auto result) { qCWarning(chatterinoIvr) - << "Failed IVR API Call!" << result.status() + << "Failed IVR API Call!" << result.formatError() << QString(result.getData()); failureCallback(); }) @@ -51,7 +51,7 @@ void IvrApi::getBulkEmoteSets(QString emoteSetList, }) .onError([failureCallback](auto result) { qCWarning(chatterinoIvr) - << "Failed IVR API Call!" << result.status() + << "Failed IVR API Call!" << result.formatError() << QString(result.getData()); failureCallback(); }) diff --git a/src/providers/RecentMessagesApi.cpp b/src/providers/RecentMessagesApi.cpp index 9fd524f68..05ee6e8c8 100644 --- a/src/providers/RecentMessagesApi.cpp +++ b/src/providers/RecentMessagesApi.cpp @@ -217,17 +217,20 @@ void RecentMessagesApi::loadRecentMessages(const QString &channelName, return Success; }) - .onError([channelPtr, onError](NetworkResult result) { + .onError([channelPtr, onError](const NetworkResult &result) { auto shared = channelPtr.lock(); if (!shared) + { return; + } qCDebug(chatterinoRecentMessages) << "Failed to load recent messages for" << shared->getName(); shared->addMessage(makeSystemMessage( - QString("Message history service unavailable (Error %1)") - .arg(result.status()))); + QStringLiteral( + "Message history service unavailable (Error: %1)") + .arg(result.formatError()))); onError(); }) diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index b1b6fb2e7..42bf51cba 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -259,23 +259,17 @@ void BttvEmotes::loadChannel(std::weak_ptr channel, shared->addMessage( makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); } - else if (result.status() == NetworkResult::timedoutStatus) - { - // TODO: Auto retry in case of a timeout, with a delay - qCWarning(chatterinoBttv) - << "Fetching BTTV emotes for channel" << channelId - << "failed due to timeout"; - shared->addMessage(makeSystemMessage( - "Failed to fetch BetterTTV channel emotes. (timed out)")); - } else { + // TODO: Auto retry in case of a timeout, with a delay + auto errorString = result.formatError(); qCWarning(chatterinoBttv) << "Error fetching BTTV emotes for channel" << channelId - << ", error" << result.status(); - shared->addMessage( - makeSystemMessage("Failed to fetch BetterTTV channel " - "emotes. (unknown error)")); + << ", error" << errorString; + shared->addMessage(makeSystemMessage( + QStringLiteral("Failed to fetch BetterTTV channel " + "emotes. (Error: %1)") + .arg(errorString))); } }) .execute(); diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 73be0b4dd..0a28cc51b 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -273,24 +273,17 @@ void FfzEmotes::loadChannel( makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); } } - else if (result.status() == NetworkResult::timedoutStatus) - { - // TODO: Auto retry in case of a timeout, with a delay - qCWarning(chatterinoFfzemotes) - << "Fetching FFZ emotes for channel" << channelID - << "failed due to timeout"; - shared->addMessage( - makeSystemMessage("Failed to fetch FrankerFaceZ channel " - "emotes. (timed out)")); - } else { + // TODO: Auto retry in case of a timeout, with a delay + auto errorString = result.formatError(); qCWarning(chatterinoFfzemotes) << "Error fetching FFZ emotes for channel" << channelID - << ", error" << result.status(); - shared->addMessage( - makeSystemMessage("Failed to fetch FrankerFaceZ channel " - "emotes. (unknown error)")); + << ", error" << errorString; + shared->addMessage(makeSystemMessage( + QStringLiteral("Failed to fetch FrankerFaceZ channel " + "emotes. (Error: %1)") + .arg(errorString))); } }) .execute(); diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index 321e43a65..a384189fa 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -386,23 +386,17 @@ void SeventvEmotes::loadChannelEmotes( makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); } } - else if (result.status() == NetworkResult::timedoutStatus) - { - // TODO: Auto retry in case of a timeout, with a delay - qCWarning(chatterinoSeventv) - << "Fetching 7TV emotes for channel" << channelId - << "failed due to timeout"; - shared->addMessage(makeSystemMessage( - "Failed to fetch 7TV channel emotes. (timed out)")); - } else { + // TODO: Auto retry in case of a timeout, with a delay + auto errorString = result.formatError(); qCWarning(chatterinoSeventv) << "Error fetching 7TV emotes for channel" << channelId - << ", error" << result.status(); - shared->addMessage( - makeSystemMessage("Failed to fetch 7TV channel " - "emotes. (unknown error)")); + << ", error" << errorString; + shared->addMessage(makeSystemMessage( + QStringLiteral("Failed to fetch 7TV channel " + "emotes. (Error: %1)") + .arg(errorString))); } }) .execute(); @@ -502,14 +496,7 @@ void SeventvEmotes::getEmoteSet( }) .onError([emoteSetId, callback = std::move(errorCallback)]( const NetworkResult &result) { - if (result.status() == NetworkResult::timedoutStatus) - { - callback("timed out"); - } - else - { - callback(QString("status: %1").arg(result.status())); - } + callback(result.formatError()); }) .execute(); } diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index b22e20669..04cf34f5d 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -390,7 +390,7 @@ void Helix::createClip(QString channelId, return Success; }) .onError([failureCallback](auto result) { - switch (result.status()) + switch (result.status().value_or(0)) { case 503: { // Channel has disabled clip-creation, or channel has made cliops only creatable by followers and the user is not a follower (or subscriber) @@ -406,7 +406,7 @@ void Helix::createClip(QString channelId, default: { qCDebug(chatterinoTwitch) - << "Failed to create a clip: " << result.status() + << "Failed to create a clip: " << result.formatError() << result.getData(); failureCallback(HelixClipError::Unknown); } @@ -477,7 +477,7 @@ void Helix::createStreamMarker( return Success; }) .onError([failureCallback](NetworkResult result) { - switch (result.status()) + switch (result.status().value_or(0)) { case 403: { // User isn't a Channel Editor, so he can't create markers @@ -495,7 +495,7 @@ void Helix::createStreamMarker( default: { qCDebug(chatterinoTwitch) << "Failed to create a stream marker: " - << result.status() << result.getData(); + << result.formatError() << result.getData(); failureCallback(HelixStreamMarkerError::Unknown); } break; @@ -638,7 +638,7 @@ void Helix::manageAutoModMessages( return Success; }) .onError([failureCallback, msgID, action](NetworkResult result) { - switch (result.status()) + switch (result.status().value_or(0)) { case 400: { // Message was already processed @@ -670,7 +670,7 @@ void Helix::manageAutoModMessages( default: { qCDebug(chatterinoTwitch) << "Failed to manage automod message: " << action - << msgID << result.status() << result.getData(); + << msgID << result.formatError() << result.getData(); failureCallback(HelixAutoModMessageError::Unknown); } break; @@ -712,7 +712,7 @@ void Helix::getCheermotes( .onError([broadcasterId, failureCallback](NetworkResult result) { qCDebug(chatterinoTwitch) << "Failed to get cheermotes(broadcaster_id=" << broadcasterId - << "): " << result.status() << result.getData(); + << "): " << result.formatError() << result.getData(); failureCallback(); }) .execute(); @@ -806,17 +806,24 @@ void Helix::updateUserChatColor( { qCWarning(chatterinoTwitch) << "Success result for updating chat color was" - << result.status() << "but we only expected it to be 204"; + << result.formatError() + << "but we only expected it to be 204"; } successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { if (message.startsWith("invalid color", @@ -849,7 +856,7 @@ void Helix::updateUserChatColor( default: { qCDebug(chatterinoTwitch) << "Unhandled error changing user color:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -882,17 +889,24 @@ void Helix::deleteChatMessages( { qCWarning(chatterinoTwitch) << "Success result for deleting chat messages was" - << result.status() << "but we only expected it to be 204"; + << result.formatError() + << "but we only expected it to be 204"; } successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 404: { // A 404 on this endpoint means message id is invalid or unable to be deleted. @@ -934,7 +948,7 @@ void Helix::deleteChatMessages( default: { qCDebug(chatterinoTwitch) << "Unhandled error deleting chat messages:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -960,17 +974,24 @@ void Helix::addChannelModerator( { qCWarning(chatterinoTwitch) << "Success result for adding a moderator was" - << result.status() << "but we only expected it to be 204"; + << result.formatError() + << "but we only expected it to be 204"; } successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 401: { if (message.startsWith("Missing scope", @@ -1022,7 +1043,7 @@ void Helix::addChannelModerator( default: { qCDebug(chatterinoTwitch) << "Unhandled error adding channel moderator:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -1048,17 +1069,24 @@ void Helix::removeChannelModerator( { qCWarning(chatterinoTwitch) << "Success result for unmodding user was" - << result.status() << "but we only expected it to be 204"; + << result.formatError() + << "but we only expected it to be 204"; } successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { if (message.compare("user is not a mod", @@ -1100,8 +1128,8 @@ void Helix::removeChannelModerator( default: { qCDebug(chatterinoTwitch) - << "Unhandled error unmodding user:" << result.status() - << result.getData() << obj; + << "Unhandled error unmodding user:" + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -1135,17 +1163,24 @@ void Helix::sendChatAnnouncement( { qCWarning(chatterinoTwitch) << "Success result for sending an announcement was" - << result.status() << "but we only expected it to be 204"; + << result.formatError() + << "but we only expected it to be 204"; } successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { // These errors are generally well formatted, so we just forward them. @@ -1178,7 +1213,7 @@ void Helix::sendChatAnnouncement( default: { qCDebug(chatterinoTwitch) << "Unhandled error sending an announcement:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -1204,17 +1239,24 @@ void Helix::addChannelVIP( { qCWarning(chatterinoTwitch) << "Success result for adding channel VIP was" - << result.status() << "but we only expected it to be 204"; + << result.formatError() + << "but we only expected it to be 204"; } successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: case 409: @@ -1256,7 +1298,7 @@ void Helix::addChannelVIP( default: { qCDebug(chatterinoTwitch) << "Unhandled error adding channel VIP:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -1282,17 +1324,24 @@ void Helix::removeChannelVIP( { qCWarning(chatterinoTwitch) << "Success result for removing channel VIP was" - << result.status() << "but we only expected it to be 204"; + << result.formatError() + << "but we only expected it to be 204"; } successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: case 409: @@ -1333,7 +1382,7 @@ void Helix::removeChannelVIP( default: { qCDebug(chatterinoTwitch) << "Unhandled error removing channel VIP:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -1371,17 +1420,24 @@ void Helix::unbanUser( { qCWarning(chatterinoTwitch) << "Success result for unbanning user was" - << result.status() << "but we only expected it to be 204"; + << result.formatError() + << "but we only expected it to be 204"; } successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { if (message.startsWith("The user in the user_id query " @@ -1437,8 +1493,8 @@ void Helix::unbanUser( default: { qCDebug(chatterinoTwitch) - << "Unhandled error unbanning user:" << result.status() - << result.getData() << obj; + << "Unhandled error unbanning user:" + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -1476,11 +1532,17 @@ void Helix::startRaid( successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { if (message.compare("The IDs in from_broadcaster_id and " @@ -1531,7 +1593,7 @@ void Helix::startRaid( default: { qCDebug(chatterinoTwitch) << "Unhandled error while starting a raid:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -1556,17 +1618,24 @@ void Helix::cancelRaid( { qCWarning(chatterinoTwitch) << "Success result for canceling the raid was" - << result.status() << "but we only expected it to be 204"; + << result.formatError() + << "but we only expected it to be 204"; } successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 401: { if (message.startsWith("Missing scope", @@ -1603,7 +1672,7 @@ void Helix::cancelRaid( default: { qCDebug(chatterinoTwitch) << "Unhandled error while canceling the raid:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -1717,18 +1786,24 @@ void Helix::updateChatSettings( { qCWarning(chatterinoTwitch) << "Success result for updating chat settings was" - << result.status() << "but we expected it to be 200"; + << result.formatError() << "but we expected it to be 200"; } auto response = result.parseJson(); successCallback(HelixChatSettings( response.value("data").toArray().first().toObject())); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { if (message.contains("must be in the range")) @@ -1775,7 +1850,7 @@ void Helix::updateChatSettings( default: { qCDebug(chatterinoTwitch) << "Unhandled error updating chat settings:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -1840,18 +1915,24 @@ void Helix::fetchChatters( { qCWarning(chatterinoTwitch) << "Success result for getting chatters was " - << result.status() << "but we expected it to be 200"; + << result.formatError() << "but we expected it to be 200"; } auto response = result.parseJson(); successCallback(HelixChatters(response)); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { failureCallback(Error::Forwarded, message); @@ -1882,7 +1963,7 @@ void Helix::fetchChatters( default: { qCDebug(chatterinoTwitch) - << "Unhandled error data:" << result.status() + << "Unhandled error data:" << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } @@ -1949,18 +2030,24 @@ void Helix::fetchModerators( { qCWarning(chatterinoTwitch) << "Success result for getting moderators was " - << result.status() << "but we expected it to be 200"; + << result.formatError() << "but we expected it to be 200"; } auto response = result.parseJson(); successCallback(HelixModerators(response)); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { failureCallback(Error::Forwarded, message); @@ -1991,7 +2078,7 @@ void Helix::fetchModerators( default: { qCDebug(chatterinoTwitch) - << "Unhandled error data:" << result.status() + << "Unhandled error data:" << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } @@ -2035,17 +2122,23 @@ void Helix::banUser(QString broadcasterID, QString moderatorID, QString userID, { qCWarning(chatterinoTwitch) << "Success result for banning a user was" - << result.status() << "but we expected it to be 200"; + << result.formatError() << "but we expected it to be 200"; } // we don't care about the response successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { if (message.startsWith("The user specified in the user_id " @@ -2099,8 +2192,8 @@ void Helix::banUser(QString broadcasterID, QString moderatorID, QString userID, default: { qCDebug(chatterinoTwitch) - << "Unhandled error banning user:" << result.status() - << result.getData() << obj; + << "Unhandled error banning user:" + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -2132,17 +2225,23 @@ void Helix::sendWhisper( { qCWarning(chatterinoTwitch) << "Success result for sending a whisper was" - << result.status() << "but we expected it to be 204"; + << result.formatError() << "but we expected it to be 204"; } // we don't care about the response successCallback(); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { if (message.startsWith("A user cannot whisper themself", @@ -2203,8 +2302,8 @@ void Helix::sendWhisper( default: { qCDebug(chatterinoTwitch) - << "Unhandled error banning user:" << result.status() - << result.getData() << obj; + << "Unhandled error banning user:" + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -2274,8 +2373,8 @@ void Helix::getChannelVIPs( if (result.status() != 200) { qCWarning(chatterinoTwitch) - << "Success result for getting VIPs was" << result.status() - << "but we expected it to be 200"; + << "Success result for getting VIPs was" + << result.formatError() << "but we expected it to be 200"; } auto response = result.parseJson(); @@ -2289,11 +2388,17 @@ void Helix::getChannelVIPs( successCallback(channelVips); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { failureCallback(Error::Forwarded, message); @@ -2333,8 +2438,8 @@ void Helix::getChannelVIPs( default: { qCDebug(chatterinoTwitch) - << "Unhandled error listing VIPs:" << result.status() - << result.getData() << obj; + << "Unhandled error listing VIPs:" + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -2370,11 +2475,17 @@ void Helix::startCommercial( successCallback(HelixStartCommercialResponse(obj)); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: { if (message.startsWith("Missing scope", @@ -2429,7 +2540,7 @@ void Helix::startCommercial( default: { qCDebug(chatterinoTwitch) << "Unhandled error starting commercial:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -2452,18 +2563,24 @@ void Helix::getGlobalBadges( { qCWarning(chatterinoTwitch) << "Success result for getting global badges was " - << result.status() << "but we expected it to be 200"; + << result.formatError() << "but we expected it to be 200"; } auto response = result.parseJson(); successCallback(HelixGlobalBadges(response)); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 401: { failureCallback(Error::Forwarded, message); @@ -2473,7 +2590,7 @@ void Helix::getGlobalBadges( default: { qCWarning(chatterinoTwitch) << "Helix global badges, unhandled error data:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -2499,18 +2616,24 @@ void Helix::getChannelBadges( { qCWarning(chatterinoTwitch) << "Success result for getting badges was " - << result.status() << "but we expected it to be 200"; + << result.formatError() << "but we expected it to be 200"; } auto response = result.parseJson(); successCallback(HelixChannelBadges(response)); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + auto obj = result.parseJson(); auto message = obj.value("message").toString(); - switch (result.status()) + switch (*result.status()) { case 400: case 401: { @@ -2521,7 +2644,7 @@ void Helix::getChannelBadges( default: { qCWarning(chatterinoTwitch) << "Helix channel badges, unhandled error data:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -2552,7 +2675,7 @@ void Helix::updateShieldMode( { qCWarning(chatterinoTwitch) << "Success result for updating shield mode was " - << result.status() << "but we expected it to be 200"; + << result.formatError() << "but we expected it to be 200"; } const auto response = result.parseJson(); @@ -2560,11 +2683,17 @@ void Helix::updateShieldMode( HelixShieldModeStatus(response["data"][0].toObject())); return Success; }) - .onError([failureCallback](auto result) { + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + const auto obj = result.parseJson(); auto message = obj["message"].toString(); - switch (result.status()) + switch (*result.status()) { case 400: { if (message.startsWith("Missing scope", @@ -2590,7 +2719,7 @@ void Helix::updateShieldMode( default: { qCWarning(chatterinoTwitch) << "Helix shield mode, unhandled error data:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } break; @@ -2619,17 +2748,23 @@ void Helix::sendShoutout( { qCWarning(chatterinoTwitch) << "Success result for sending shoutout was " - << result.status() << "but we expected it to be 204"; + << result.formatError() << "but we expected it to be 204"; } successCallback(); return Success; }) - .onError([failureCallback](NetworkResult result) -> void { + .onError([failureCallback](const NetworkResult &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + const auto obj = result.parseJson(); auto message = obj["message"].toString(); - switch (result.status()) + switch (*result.status()) { case 400: { if (message.startsWith("The broadcaster may not give " @@ -2692,7 +2827,7 @@ void Helix::sendShoutout( default: { qCWarning(chatterinoTwitch) << "Helix send shoutout, unhandled error data:" - << result.status() << result.getData() << obj; + << result.formatError() << result.getData() << obj; failureCallback(Error::Unknown, message); } } diff --git a/src/singletons/Updates.cpp b/src/singletons/Updates.cpp index c54d63e7b..b25e52819 100644 --- a/src/singletons/Updates.cpp +++ b/src/singletons/Updates.cpp @@ -128,8 +128,8 @@ void Updates::installUpdates() auto *box = new QMessageBox( QMessageBox::Information, "Chatterino Update", QStringLiteral("The update couldn't be downloaded " - "(HTTP status %1).") - .arg(result.status())); + "(Error: %1).") + .arg(result.formatError())); box->setAttribute(Qt::WA_DeleteOnClose); box->exec(); return Failure; @@ -189,8 +189,8 @@ void Updates::installUpdates() auto *box = new QMessageBox( QMessageBox::Information, "Chatterino Update", QStringLiteral("The update couldn't be downloaded " - "(HTTP status %1).") - .arg(result.status())); + "(Error: %1).") + .arg(result.formatError())); box->setAttribute(Qt::WA_DeleteOnClose); box->exec(); return Failure; diff --git a/src/util/NuulsUploader.cpp b/src/util/NuulsUploader.cpp index 79f7b3794..8627ef8fd 100644 --- a/src/util/NuulsUploader.cpp +++ b/src/util/NuulsUploader.cpp @@ -208,7 +208,7 @@ void uploadImageToNuuls(RawImageData imageData, ChannelPtr channel, .onError([channel](NetworkResult result) -> bool { auto errorMessage = QString("An error happened while uploading your image: %1") - .arg(result.status()); + .arg(result.formatError()); // Try to read more information from the result body auto obj = result.parseJson(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 09a9828bc..06c254ba9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp ${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.cpp ${CMAKE_CURRENT_LIST_DIR}/src/NetworkRequest.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/NetworkResult.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ChatterSet.cpp ${CMAKE_CURRENT_LIST_DIR}/src/HighlightPhrase.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Emojis.cpp diff --git a/tests/src/NetworkRequest.cpp b/tests/src/NetworkRequest.cpp index 2bcb5da9a..048ed1c91 100644 --- a/tests/src/NetworkRequest.cpp +++ b/tests/src/NetworkRequest.cpp @@ -210,7 +210,9 @@ TEST(NetworkRequest, TimeoutTimingOut) .onError([&waiter, url](const NetworkResult &result) { qDebug() << QTime::currentTime().toString() << "timeout request finish error"; - EXPECT_EQ(result.status(), NetworkResult::timedoutStatus); + EXPECT_EQ(result.error(), + NetworkResult::NetworkError::TimeoutError); + EXPECT_EQ(result.status(), std::nullopt); waiter.requestDone(); }) @@ -267,7 +269,9 @@ TEST(NetworkRequest, FinallyCallbackOnTimeout) }) .onError([&](const NetworkResult &result) { onErrorCalled = true; - EXPECT_EQ(result.status(), NetworkResult::timedoutStatus); + EXPECT_EQ(result.error(), + NetworkResult::NetworkError::TimeoutError); + EXPECT_EQ(result.status(), std::nullopt); }) .finally([&] { finallyCalled = true; diff --git a/tests/src/NetworkResult.cpp b/tests/src/NetworkResult.cpp new file mode 100644 index 000000000..6bafc3a83 --- /dev/null +++ b/tests/src/NetworkResult.cpp @@ -0,0 +1,48 @@ +#include "common/NetworkResult.hpp" + +#include + +using namespace chatterino; + +using Error = NetworkResult::NetworkError; + +namespace { + +void checkResult(const NetworkResult &res, Error error, + std::optional status, const QString &formatted) +{ + ASSERT_EQ(res.error(), error); + ASSERT_EQ(res.status(), status); + ASSERT_EQ(res.formatError(), formatted); +} + +} // namespace + +TEST(NetworkResult, NoError) +{ + checkResult({Error::NoError, 200, {}}, Error::NoError, 200, "200"); + checkResult({Error::NoError, 202, {}}, Error::NoError, 202, "202"); + + // no status code + checkResult({Error::NoError, {}, {}}, Error::NoError, std::nullopt, + "NoError"); +} + +TEST(NetworkResult, Errors) +{ + checkResult({Error::TimeoutError, {}, {}}, Error::TimeoutError, + std::nullopt, "TimeoutError"); + checkResult({Error::RemoteHostClosedError, {}, {}}, + Error::RemoteHostClosedError, std::nullopt, + "RemoteHostClosedError"); + + // status code takes precedence + checkResult({Error::TimeoutError, 400, {}}, Error::TimeoutError, 400, + "400"); +} + +TEST(NetworkResult, InvalidError) +{ + checkResult({static_cast(-1), {}, {}}, static_cast(-1), + std::nullopt, "unknown error (-1)"); +}