From 1f96b1b7ed327be9de1609fd5d13bc59fbbc8ac0 Mon Sep 17 00:00:00 2001 From: Andreas Loth Date: Tue, 19 Sep 2023 12:17:18 +0200 Subject: [PATCH] Axivion: Analyze Dashboard server response for errors Change-Id: Id66c3ad5d8a6c7d73e7ad781893c936b0829cfbf Reviewed-by: Reviewed-by: hjk --- src/plugins/axivion/CMakeLists.txt | 1 + src/plugins/axivion/axivion.qbs | 8 +- src/plugins/axivion/axivionplugin.cpp | 3 +- .../axivion/dashboard/dashboardclient.cpp | 55 +++++++-- .../axivion/dashboard/dashboardclient.h | 3 +- src/plugins/axivion/dashboard/error.cpp | 107 ++++++++++++++++++ src/plugins/axivion/dashboard/error.h | 86 ++++++++++++++ 7 files changed, 247 insertions(+), 16 deletions(-) create mode 100644 src/plugins/axivion/dashboard/error.cpp create mode 100644 src/plugins/axivion/dashboard/error.h diff --git a/src/plugins/axivion/CMakeLists.txt b/src/plugins/axivion/CMakeLists.txt index c71df674750..ff089f1a98e 100644 --- a/src/plugins/axivion/CMakeLists.txt +++ b/src/plugins/axivion/CMakeLists.txt @@ -14,4 +14,5 @@ add_qtc_plugin(Axivion dashboard/dto.cpp dashboard/dto.h dashboard/concat.cpp dashboard/concat.h dashboard/dashboardclient.cpp dashboard/dashboardclient.h + dashboard/error.h dashboard/error.cpp ) diff --git a/src/plugins/axivion/axivion.qbs b/src/plugins/axivion/axivion.qbs index bdd20a89349..1fb4dd7bfa8 100644 --- a/src/plugins/axivion/axivion.qbs +++ b/src/plugins/axivion/axivion.qbs @@ -26,14 +26,12 @@ QtcPlugin { "axivionsettings.cpp", "axivionsettings.h", "axiviontr.h", - "dashboard/dashboardclient.cpp", - "dashboard/dashboardclient.h", ] cpp.includePaths: base.concat(["."]) // needed for the generated stuff below Group { - name: "Generated DTOs" + name: "Dashboard Communication" prefix: "dashboard/" files: [ @@ -41,6 +39,10 @@ QtcPlugin { "concat.h", "dto.cpp", "dto.h", + "dashboardclient.cpp", + "dashboardclient.h", + "error.cpp", + "error.h", ] } } diff --git a/src/plugins/axivion/axivionplugin.cpp b/src/plugins/axivion/axivionplugin.cpp index 0c09ecb475d..2a6af5a5cad 100644 --- a/src/plugins/axivion/axivionplugin.cpp +++ b/src/plugins/axivion/axivionplugin.cpp @@ -259,7 +259,8 @@ void AxivionPluginPrivate::handleProjectInfo(DashboardClient::RawProjectInfo raw { m_runningQuery = false; if (!rawInfo) { - Core::MessageManager::writeFlashing(QStringLiteral(u"Axivion: ") + rawInfo.error()); + Core::MessageManager::writeFlashing( + QStringLiteral(u"Axivion: %1").arg(rawInfo.error().message())); return; } m_currentProjectInfo = std::make_shared(std::move(rawInfo.value())); diff --git a/src/plugins/axivion/dashboard/dashboardclient.cpp b/src/plugins/axivion/dashboard/dashboardclient.cpp index 7e1a2761ec3..e0c5bf7a124 100644 --- a/src/plugins/axivion/dashboard/dashboardclient.cpp +++ b/src/plugins/axivion/dashboard/dashboardclient.cpp @@ -30,13 +30,17 @@ static void deleteLater(QObject *obj) obj->deleteLater(); } -using RawBody = Utils::expected_str>; +using RawBody = Utils::expected, Error>; + +static constexpr int httpStatusCodeOk = 200; +static constexpr QLatin1String jsonContentType{ "application/json" }; class RawBodyReader final { public: - RawBodyReader(std::shared_ptr reply) - : m_reply(std::move(reply)) + RawBodyReader(std::shared_ptr reply, QAnyStringView expectedContentType) + : m_reply(std::move(reply)), + m_expectedContentType(expectedContentType) { } ~RawBodyReader() { } @@ -44,19 +48,47 @@ public: RawBody operator()() { QNetworkReply::NetworkError error = m_reply->error(); - if (error != QNetworkReply::NetworkError::NoError) - return tl::make_unexpected(QString::number(error) - + QLatin1String(": ") - + m_reply->errorString()); - return DataWithOrigin(m_reply->url(), m_reply->readAll()); + int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + QString contentType = m_reply->header(QNetworkRequest::ContentTypeHeader) + .toString() + .split(';') + .constFirst() + .trimmed() + .toLower(); + if (error == QNetworkReply::NetworkError::NoError + && statusCode == httpStatusCodeOk + && contentType == m_expectedContentType) { + return DataWithOrigin(m_reply->url(), m_reply->readAll()); + } + if (contentType == jsonContentType) { + try { + return tl::make_unexpected(DashboardError( + m_reply->url(), + statusCode, + m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(), + Dto::ErrorDto::deserialize(m_reply->readAll()))); + } catch (const Dto::invalid_dto_exception &) { + // ignore + } + } + if (statusCode != 0) { + return tl::make_unexpected(HttpError( + m_reply->url(), + statusCode, + m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(), + QString::fromUtf8(m_reply->readAll()))); // encoding? + } + return tl::make_unexpected( + NetworkError(m_reply->url(), error, m_reply->errorString())); } private: std::shared_ptr m_reply; + QAnyStringView m_expectedContentType; }; template -static Utils::expected_str> RawBodyParser(RawBody rawBody) +static Utils::expected, Error> RawBodyParser(RawBody rawBody) { if (!rawBody) return tl::make_unexpected(std::move(rawBody.error())); @@ -65,7 +97,8 @@ static Utils::expected_str> RawBodyParser(RawBody rawBody) return DataWithOrigin(std::move(rawBody.value().origin), std::move(data)); } catch (const Dto::invalid_dto_exception &e) { - return tl::make_unexpected(QString::fromUtf8(e.what())); + return tl::make_unexpected(GeneralError(std::move(rawBody.value().origin), + QString::fromUtf8(e.what()))); } } @@ -91,7 +124,7 @@ QFuture DashboardClient::fetchProjectInfo(const std::shared_ptr reply{ this->m_networkAccessManager.get(request), deleteLater }; return QtFuture::connect(reply.get(), &QNetworkReply::finished) .onCanceled(reply.get(), [reply] { reply->abort(); }) - .then(RawBodyReader(reply)) + .then(RawBodyReader(reply, jsonContentType)) .then(QtFuture::Launch::Async, &RawBodyParser); } diff --git a/src/plugins/axivion/dashboard/dashboardclient.h b/src/plugins/axivion/dashboard/dashboardclient.h index eda623b878b..eb9d58c799f 100644 --- a/src/plugins/axivion/dashboard/dashboardclient.h +++ b/src/plugins/axivion/dashboard/dashboardclient.h @@ -8,6 +8,7 @@ */ #include "dashboard/dto.h" +#include "dashboard/error.h" #include #include @@ -32,7 +33,7 @@ class DashboardClient { public: using ProjectInfo = DataWithOrigin; - using RawProjectInfo = Utils::expected_str; + using RawProjectInfo = Utils::expected; DashboardClient(Utils::NetworkAccessManager &networkAccessManager); diff --git a/src/plugins/axivion/dashboard/error.cpp b/src/plugins/axivion/dashboard/error.cpp new file mode 100644 index 00000000000..f583a6f7815 --- /dev/null +++ b/src/plugins/axivion/dashboard/error.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022-current by Axivion GmbH + * https://www.axivion.com/ + * + * SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + */ + +#include "dashboard/error.h" + +#include +#include + +namespace Axivion::Internal +{ + +CommunicationError::CommunicationError(QUrl replyUrl) + : replyUrl(std::move(replyUrl)) +{ +} + +GeneralError::GeneralError(QUrl replyUrl, QString message) + : CommunicationError(replyUrl), + message(std::move(message)) +{ +} + +NetworkError::NetworkError(QUrl replyUrl, + QNetworkReply::NetworkError networkError, + QString networkErrorString) + : CommunicationError(std::move(replyUrl)), + networkError(std::move(networkError)), + networkErrorString(std::move(networkErrorString)) +{ +} + +HttpError::HttpError(QUrl replyUrl, int httpStatusCode, QString httpReasonPhrase, QString body) + : CommunicationError(std::move(replyUrl)), + httpStatusCode(httpStatusCode), + httpReasonPhrase(std::move(httpReasonPhrase)), + body(std::move(body)) +{ +} + +DashboardError::DashboardError(QUrl replyUrl, int httpStatusCode, QString httpReasonPhrase, Dto::ErrorDto error) + : CommunicationError(std::move(replyUrl)), + httpStatusCode(httpStatusCode), + httpReasonPhrase(std::move(httpReasonPhrase)), + dashboardVersion(std::move(error.dashboardVersionNumber)), + type(std::move(error.type)), + message(std::move(error.message)) +{ +} + +Error::Error(GeneralError error) : m_error(std::move(error)) +{ +} + +Error::Error(NetworkError error) : m_error(std::move(error)) +{ +} + +Error::Error(HttpError error) : m_error(std::move(error)) +{ +} + +Error::Error(DashboardError error) : m_error(std::move(error)) +{ +} + +// https://www.cppstories.com/2018/09/visit-variants/ +template struct overloaded : Ts... { using Ts::operator()...; }; +template overloaded(Ts...) -> overloaded; // line not needed in C++20... + +QString Error::message() const +{ + return std::visit( + overloaded{ + [](const GeneralError &error) { + return QStringLiteral(u"GeneralError (%1) %2") + .arg(error.replyUrl.toString(), + error.message); + }, + [](const NetworkError &error) { + return QStringLiteral(u"NetworkError (%1) %2: %3") + .arg(error.replyUrl.toString(), + QString::number(error.networkError), + error.networkErrorString); + }, + [](const HttpError &error) { + return QStringLiteral(u"HttpError (%1) %2: %3\n%4") + .arg(error.replyUrl.toString(), + QString::number(error.httpStatusCode), + error.httpReasonPhrase, + error.body); + }, + [](const DashboardError &error) { + return QStringLiteral(u"DashboardError (%1) [%2 %3] %4: %5") + .arg(error.replyUrl.toString(), + QString::number(error.httpStatusCode), + error.httpReasonPhrase, + error.type, + error.message); + }, + }, this->m_error); +} + +} // namespace Axivion::Internal diff --git a/src/plugins/axivion/dashboard/error.h b/src/plugins/axivion/dashboard/error.h new file mode 100644 index 00000000000..4e2a434ab81 --- /dev/null +++ b/src/plugins/axivion/dashboard/error.h @@ -0,0 +1,86 @@ +#pragma once + +/* + * Copyright (C) 2022-current by Axivion GmbH + * https://www.axivion.com/ + * + * SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + */ + +#include "dashboard/dto.h" + +#include + +#include + +namespace Axivion::Internal +{ + +class CommunicationError +{ +public: + QUrl replyUrl; + + CommunicationError(QUrl replyUrl); +}; + +class GeneralError : public CommunicationError +{ +public: + QString message; + + GeneralError(QUrl replyUrl, QString message); +}; + +class NetworkError : public CommunicationError +{ +public: + QNetworkReply::NetworkError networkError; + QString networkErrorString; + + NetworkError(QUrl replyUrl, QNetworkReply::NetworkError networkError, QString networkErrorString); +}; + +class HttpError : public CommunicationError +{ +public: + int httpStatusCode; + QString httpReasonPhrase; + QString body; + + HttpError(QUrl replyUrl, int httpStatusCode, QString httpReasonPhrase, QString body); +}; + +class DashboardError : public CommunicationError +{ +public: + int httpStatusCode; + QString httpReasonPhrase; + std::optional dashboardVersion; + QString type; + QString message; + + DashboardError(QUrl replyUrl, int httpStatusCode, QString httpReasonPhrase, Dto::ErrorDto error); +}; + +class Error +{ +public: + Error(GeneralError error); + + Error(NetworkError error); + + Error(HttpError error); + + Error(DashboardError error); + + QString message() const; + +private: + std::variant m_error; +}; + +} // namespace Axivion::Internal